facebookarchive / redux-react-hook

React Hook for accessing state and dispatch from a Redux store
MIT License
2.16k stars 103 forks source link

[Question] Why multi store subscriptions? #33

Closed hnordt closed 5 years ago

hnordt commented 5 years ago

Just for curiosity, why you've opted for a multi-store subscription model instead of subscribing to the store in a top-level Provider?

Something like:

const ReduxStateContext = React.createContext()

function ReduxStateProvider({ store, children }) {
  const [state, setState] = React.useState(() => store.getState())

  useEffect(() => {
    let willUnsubscribe = false

    const checkForUpdates = () => {
      if (willUnsubscribe) return

      setState(prevState => {
        const nextState = store.getState()
        return shallowEqual(prevState, nextState) ? prevState : nextState
      })
    }

    checkForUpdates()

    const unsubscribe = store.subscribe(checkForUpdates)

    return () => {
      willUnsubscribe = true
      unsubscribe()
    }
  }, [store])

  return (
    <ReduxStateContext.Provider value={state}>
      {children}
    </ReduxStateContext.Provider>
  )
}

export function useMappedState(mapState) {
  const state = useContext(ReduxStateContext)

  const [derivedState, setDerivedState] = useState(() => mapState(state))

  useEffect(() => {
    setDerivedState(prevDerivedState => {
      const nextDerivedState = mapState(state)
      return shallowEqual(prevDerivedState, nextDerivedState)
        ? prevDerivedState
        : nextDerivedState
    })
  }, [state])

  // It might not even need useEffect() 🤔 (getDerivedStateFromProps)
  setDerivedState(prevDerivedState => {
    const nextDerivedState = mapState(state)
    return shallowEqual(prevDerivedState, nextDerivedState)
      ? prevDerivedState
      : nextDerivedState
  })

  return derivedState
}

Only the ReduxStateProvider subscribes to store updates, then it passes the update down to all consumers. Consumers have a chance to bail out by comparing prevDerivedState with nextDerivedState.

brunolemos commented 5 years ago

const state = useContext(ReduxStateContext)

Every single action that is dispatched via redux and change any place of the state would trigger a rerender on all components that are using useMappedState simply because it's using the line above.

Context updates triggers a rerender just like useState updates and you can't bail out of context updates yet, see item 2: https://github.com/facebook/react/issues/14110

hnordt commented 5 years ago

But then the consumer will bail out via setDerivedState. This same problem happens to the multi store approach, because every hook subscribes to the store.

hnordt commented 5 years ago

Hum, I think I got it. The useContext will make the component using this hook to rerender, the setDerivedState bails out itself only.

ianobermiller commented 5 years ago

See also the react-redux roadmap on why they are going back to using their own subscription rather than context: https://github.com/reduxjs/react-redux/issues/1177