Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Rewriting mapPropsStream with Hooks or new Lifecycle Methods #783

Open
jameslaneconkling opened this issue Aug 30, 2019 · 4 comments
Open

Comments

@jameslaneconkling
Copy link

jameslaneconkling commented Aug 30, 2019

As a result of React's deprecation of the componentWillMount and componentWillReceiveProps lifecycle hooks, recompose's mapPropsStream now warns

Warning: componentWillMount has been renamed, and is not recommended for use. See https://fb.me/react-async-component-lifecycle-hooks for details.

I've been looking for an equivalent implementation using either Hooks, the new Suspense API, or new lifecycle methods, without much luck. The following works using the unsafe lifecycle methods

const mapPropsStream = (project) => (wrappedComponent) =>
  class MapPropsStream extends Component {
    
    state = { mappedProps: undefined }
    props$ = new Subject()

    componentDidMount() {
      this.subscription = this.props$.pipe(startWith(this.props), project).subscribe((mappedProps) => {
        this.setState({ mappedProps })
      })
    }

    UNSAFE_componentWillReceiveProps(nextProps) {
      this.props$.next(nextProps)
    }

    shouldComponentUpdate(props, state) {
      return this.state.mappedProps !== state.mappedProps
    }

    componentWillUnmount() {
      this.subscription.unsubscribe()
    }

    render() {
      return this.state.mappedProps === undefined ?
        null :
        createElement(wrappedComponent, this.state.mappedProps)
    }
  }

However, I haven't been able to accomplish the same thing without UNSAFE_componentWillReceiveProps. Given that renders are triggered by changes to the props$ props stream, and not the props themselves, I suspect that the Suspense API could be helpful. Curious if anyone else is tackling the same issue.

@viztor
Copy link

viztor commented Aug 31, 2019

the author has discontinued support for recompose and recommended react hook as a replacement.

@jameslaneconkling
Copy link
Author

Understood, and I think that's a fine choice. I'm just not sure how (or even if, given the current state of the hooks API) it would be possible to recreate the functionality of mapPropsStream using hooks.

At issue is delaying the component's render from when it receives props to when the observable emits (which may be immediate or not). The current recompose implementation, and the above simplified reimplementation, essentially achieve that by using shouldComponentUpdate. Deferring rendering is not something that's supported by hooks atm, though I suspect the suspense API is intended for this type of use case.

@gemma-ferreras
Copy link

@jameslaneconkling Did you manage to recreate the functionality of mapPropsStream using hooks? I am looking for this

@jameslaneconkling
Copy link
Author

jameslaneconkling commented Oct 14, 2019

@gemma-ferreras the solution I've ended up w/ looks like:

const useStream = <T, R>(
  project: (stream$: Observable<T>) => Observable<R>,
  data: T,
): R | undefined => {
  const prev = useRef<T>()
  const stream$ = useRef(new Subject<T>())
  const emit = useRef<R>()
  const synchronous = useRef(true)
  const [_, rerender] = useState(false)

  useLayoutEffect(() => {
    const subscription = stream$.current.pipe(project).subscribe({
      next: (next) => {
        emit.current = next
        if (!synchronous.current) {
          rerender((prev) => !prev)
        }
      }
    })

    stream$.current.next(data)
    return () => subscription.unsubscribe()
  }, [])

  synchronous.current = true
  if (prev.current !== data) {
    emit.current = undefined
    stream$.current.next(data)
  }
  prev.current = data
  synchronous.current = false

  return emit.current
}

It's a little bit wordier than I'd hoped, but essentially subscribes to a stream for the lifecycle of the component, while ensuring that synchronous emits don't render twice. To use:

export const Widget: SFC<{}> = () => {
  const [channel, setChannel] = useState('friend-list')
  const selectChannel = useCallback(({ target: { value } }) => setChannel(value), [])
  const next = useStream((stream$) => stream$.pipe(
    switchMap(() => interval(500).pipe(
      startWith(-1),
      scan<number, number[]>((data) => [...data, Math.floor(Math.random() * 10)], []),
      take(4),
    )),
  ), channel)

  return el('div', null,
    el('div', null,
      el('select', { value: channel, onChange: selectChannel },
       el('option', { value: 'friend-list' }, 'Friends'),
       el('option', { value: 'enemy-list' }, 'Enemies'),
       el('option', { value: 'grocery-list' }, 'Groceries'))),
    el('h1', null, channel),
    el('ul', null, ...(next || []).map((item, idx) => (
      el('li', { key: idx }, item))))
  )
}

(accidentally fat finger closed this--just reopened)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants