reduxjs/react-redux

Discussion: Potential hooks API design

markerikson opened this issue Β· 194 comments

Let's use this thread to discuss actual design considerations for an actual hooks API.

Prior references:

Along those lines, I've collated a spreadsheet listing ~30 different unofficial useRedux-type hooks libraries.

update

I've posted a summary of my current thoughts and a possible path forward.

Based on my experiments I've come up with following wishlist for the official redux hooks api:

Provide low level primitives

  • useMapState() - with setState like === check based render bailout
    • I think think this is essential for empowering userland wrappers
  • useDispatch() - just return the dispatch function
  • useStore() - Too powerful?

Maybe these higher level APIs

  • useActionCreators() takes an actions creators object and binds it to dispatch (memoized)
  • Variation of useMapState() with shallow equal check
  • useSelector() - reselect like helper
    • This might mitigate the need for the shallow equal check in the useMapState primitive
    • I have an example implementation of this in my redux-hooks lib

Designed for good TypeScript support. TypeScript is growing like crazy and the HOC based connector is and has been pain for TypeScript users. This is an awesome opportunity to serve TS users propertly.


For the curious I would engourage you to try the hook bindings here

https://github.com/epeli/redux-hooks

It's more than a toy as it attempts to actually implement all the performance requirements needed for real world usage and I would really appreciate any feedback because feedback on it would help the design of these official ones too.

There's a similar project in the Facebook incubator: https://github.com/facebookincubator/redux-react-hook

Personally I lean towards not providing an API that's expected to produce objects at all, but rather a separate invocation for each selector or action creator you want to use.

// user augments this from outside,
// or we need some other trick to pass out-of-band type information
interface StoreState {}

// 2nd argument for these is like a useMemo argument,
// but defaults to [1st argument]. The reasoning is that
// you usually use selectors that were defined outside the
// component if they're 1-ary / creators defined outside
// the component if they're 0-ary.

// one useSelector per value you want to get
// it, of course, also implicitly depends on the
// context store's getState().
function useSelector<T>(
  selector: (state: StoreState) => T,
  deps?: ReadonlyArray<unknown>
): T

// these return their argument but bound to dispatch
// the implementation is just a memoized version of something like
//   return typeof arg1 === 'function'
//     ? (...args) => dispatch(arg1(...args))
//     : () => dispatch(arg1)
// but the types are way more complicated

// first overload for thunk action creators
function useAction<
  T extends (
    ...args: any[]
  ) => (dispatch: any, getState: () => StoreState, extraArgument: any) => any
>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T extends (...args: infer A) => (...args: any[]) => infer R
  ? (...args: A) => R
  : never
// second overload for regular action creators
function useAction<T extends (...args: any[]) => any>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T
// lastly expect a regular action
function useAction<T extends { type: string }>(
  action: T,
  deps?: ReadonlyArray<unknown>
): () => T

This does have the benefit of never giving you direct access to dispatch, though! Always using bound dispatchers feels way more ergonomic to me. If you want to improve usability further (such as binding certain arguments of a multi-argument action creator) you could always wrap either the input or the output in another useMemo.

This would also have the side-effect of creating a separate subscription per useSelector, though. I don't know if that's a relevant performance consideration or not.

I had an idea to share subscriptions between useSelector calls but it feels redundant:

// fake const, only exists for creating a named type
declare const __SubscriptionToken: unique symbol
type Subscription = typeof __SubscriptionToken

// creates a ref (what the Subscription actually is) and returns it
function useSubscription(): Subscription
// adds itself to a list of selectors the subscription updates which is...
// ...reimplementing subscriptions on top of a subscription?
function useSelector<T>(
  subscription: Subscription,
  selector: (state: StoreState) => T
): T

The complicated part is when you have a subscription value that depends on the result of other subscriptions -- but you only need one of the subscriptions to update for the component to rerender, and at that point the other selectors will be re-invoked when the useSelector is reached.

If you really want to you can also just return an object but then you have to handle memoization yourself and you can't use useMemo (directly) for it.

const mySelector = useMemo(() => {
  let previous
  return (state: StoreState) => {
    const result = { a: state.a, b: state.a && state.b }
    if (!previous || previous.a !== state.a || previous.b !== state.b) {
      previous = result
    }
    return previous
  }
}, [])

const { a, b } = useSelector(mySelector)

I also thought of a possible effect-like API but it feels dirty to use. It's "too global" as it's not necessarily coupled to your component; or even if it is, what would it mean to have multiple copies of this component mounted?

function useStoreEffect(
  effect: (state: StoreState) => void | (() => void | undefined),
  // defaults to () => undefined
  deps?: (state: StoreState) => ReadonlyArray<unknown> | undefined
): void

It's like a useEffect but it'd also be invoked outside the React render cycle if the store state changed. Probably too low-level / dangerous, but is roughly the equivalent of getting the store from the context and calling subscribe yourself.

Thinking about this as well and would suggest:

  • useSelect which would copy the select effect API from sagas. That would let you use your existing map state to props functions with no real changes.
  • useDispatch which would wrap a call to bindActionCreators letting you pass either an action creator, or object to create dispatch functions.

Both hooks would use an identity function as the default first argument so the effect of calling them without arguments would be to return the entire state, or a dispatch function respectively.

I think there's lots of room for building on top of these two base hooks but why not start super simple and let the community evolve some patterns?

Partial typescript API (doing this from my phone, so excuse any oddities)

interface useSelect {
  <S>(): S;
  <S, R>(selector: (state: S) => R): R;
  <S, P, R>(selector: (state: A, params: P, ...args: any[]) => R, params: P, ...args: any[]): R
}

interface useDispatch {
  (): Dispatch<AnyAction>;
  <A extends Action = AnyAction>(actionCreator: ActionCreator<A>): ActionCreator<A>;
  <O extends ActionCreatorMap>(actionCreators: O): O;
}

Full implementation (sans tests, examples, etc.) in this Gist - https://gist.github.com/chris-pardy/6ff60fdae7404f5745a865423989e0db

Here's an interesting API idea: Passive state mapping hook that does not subscribe to store changes at all. It only executes when the deps change.

Implementation is basically this:

function usePassiveMapState(mapState, deps) {
    const store = useStore();
    return useMemo(() => mapState(store.getState()), deps);
}

It makes no sense as a standalone hook but when combined with an active hook it opens up a whole new world of optimization techniques.

Example:

const shop = useMapState(state => state.shops[shopId]);

// Shop products is updated only when the shop itself
// has been updated. So this generates the productNames
// array only when the shop has updated. 
const productNames = usePassiveMapState(
    state => state.shop[shopId].products.map(p => p.name),
    [shop],
);

I don't think you can get more efficient than that. Pretty readable too.

Pretty much a microptimization but avoiding new references can save renders downstream from pure components.

This is available for testing here.

Personally I lean towards not providing an API that's expected to produce objects at all, but rather a separate invocation for each selector or action creator you want to use.

// user augments this from outside,
// or we need some other trick to pass out-of-band type information
interface StoreState {}

// 2nd argument for these is like a useMemo argument,
// but defaults to [1st argument]. The reasoning is that
// you usually use selectors that were defined outside the
// component if they're 1-ary / creators defined outside
// the component if they're 0-ary.

// one useSelector per value you want to get
// it, of course, also implicitly depends on the
// context store's getState().
function useSelector<T>(
  selector: (state: StoreState) => T,
  deps?: ReadonlyArray<unknown>
): T

// these return their argument but bound to dispatch
// the implementation is just a memoized version of something like
//   return typeof arg1 === 'function'
//     ? (...args) => dispatch(arg1(...args))
//     : () => dispatch(arg1)
// but the types are way more complicated

// first overload for thunk action creators
function useAction<
  T extends (
    ...args: any[]
  ) => (dispatch: any, getState: () => StoreState, extraArgument: any) => any
>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T extends (...args: infer A) => (...args: any[]) => infer R
  ? (...args: A) => R
  : never
// second overload for regular action creators
function useAction<T extends (...args: any[]) => any>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T
// lastly expect a regular action
function useAction<T extends { type: string }>(
  action: T,
  deps?: ReadonlyArray<unknown>
): () => T

This does have the benefit of never giving you direct access to dispatch, though! Always using bound dispatchers feels way more ergonomic to me. If you want to improve usability further (such as binding certain arguments of a multi-argument action creator) you could always wrap either the input or the output in another useMemo.

This would also have the side-effect of creating a separate subscription per useSelector, though. I don't know if that's a relevant performance consideration or not.

I had an idea to share subscriptions between useSelector calls but it feels redundant:

// fake const, only exists for creating a named type
declare const __SubscriptionToken: unique symbol
type Subscription = typeof __SubscriptionToken

// creates a ref (what the Subscription actually is) and returns it
function useSubscription(): Subscription
// adds itself to a list of selectors the subscription updates which is...
// ...reimplementing subscriptions on top of a subscription?
function useSelector<T>(
  subscription: Subscription,
  selector: (state: StoreState) => T
): T

The complicated part is when you have a subscription value that depends on the result of other subscriptions -- but you only need one of the subscriptions to update for the component to rerender, and at that point the other selectors will be re-invoked when the useSelector is reached.

If you really want to you can also just return an object but then you have to handle memoization yourself and you can't use useMemo (directly) for it.

const mySelector = useMemo(() => {
  let previous
  return (state: StoreState) => {
    const result = { a: state.a, b: state.a && state.b }
    if (!previous || previous.a !== state.a || previous.b !== state.b) {
      previous = result
    }
    return previous
  }
}, [])

const { a, b } = useSelector(mySelector)

I'm for this API a lot. On occasions, you need the dispatch (for dynamic actions that can't be treated with actionCreators), so I would add useDispatch.
I think this library should focus on the basic API to allow developers to extend with custom hooks. So caching/side-effect etc. should not be included

Personally I lean towards not providing an API that's expected to produce objects at all, but rather a separate invocation for each selector or action creator you want to use.

100% Agree on this, I think this is the direction things should generally be going with hooks, and it seems to jive with what facebook did with useState.

// these return their argument but bound to dispatch
// the implementation is just a memoized version of something like
//   return typeof arg1 === 'function'
//     ? (...args) => dispatch(arg1(...args))
//     : () => dispatch(arg1)
// but the types are way more complicated

// first overload for thunk action creators
function useAction<
  T extends (
    ...args: any[]
  ) => (dispatch: any, getState: () => StoreState, extraArgument: any) => any
>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T extends (...args: infer A) => (...args: any[]) => infer R
  ? (...args: A) => R
  : never
// second overload for regular action creators
function useAction<T extends (...args: any[]) => any>(
  actionCreator: T,
  deps?: ReadonlyArray<unknown>
): T
// lastly expect a regular action
function useAction<T extends { type: string }>(
  action: T,
  deps?: ReadonlyArray<unknown>
): () => T

This feels overwrought, I suggested a simple wrapper around bindActionCreators but even if that's not exactly the API, just getting a dispatch function feels like the right level of simplicity. Something that needs to handle Thunk action creators feels overwrought.

I think it's worth going all the way back to issue #1 as a reference. Dan laid out a list of constraints that the new in-progress React-Redux API would need to follow. Here's that list:

Common pain points:

  • Not intuitive how way to separate smart and dumb components with <Connector>, @connect
  • You have to manually bind action creators with bindActionCreators helper which some don't like
  • Too much nesting for small examples (<Provider>, <Connector> both need function children)

Let's go wild here. Post your alternative API suggestions.

They should satisfy the following criteria:

  • Some component at the root must hold the store instance. (Akin to <Provider>)
  • It should be possible to connect to state no matter how deep in the tree
  • It should be possible to select the state you're interested in with a select function
  • Smart / dumb components separation needs to be encouraged
  • There should be one obvious way to separate smart / dumb components
  • It should be obvious how to turn your functions into action creators
  • Smart components should probably be able to react to updates to the state in componentDidUpdate
  • Smart components' select function needs to be able to take their props into account
  • Smart component should be able to do something before/after dumb component dispatches an action
  • We should have shouldComponentUpdate wherever we can

Obviously a lot of that isn't exactly relevant for hooks, but which ones are useful, and what other constraints might be good goals?

Feels like most of those original criteria are still relevant. I would rephrase:

  • Smart components should probably be able to react to updates to the state in componentDidUpdate
  • We should have shouldComponentUpdate wherever we can

As "shouldn't impact performance".

I'm concerned that hooks would be the ultimate foot-gun for:

  • Smart / dumb components separation needs to be encouraged

But I'm not sure there's a good solution other than lots of evangelizing about the benefits of separation of concerns.

  • Smart / dumb components separation needs to be encouraged

I think this actually becomes less clear with hooks regardless. I think hooks makes it easier to understand and separate smart container vs dumb presentational components but the effort has to be conscious.


PresentationalComponent.js

export default function PresentationalComponent () {
  return // ...
}

connect HOC

// connect container
import PresentationalComponent from 'blah/PresentationalComponent';

export default connect(
  // etc...
)(PresentationalComponent);

hooks

Also addressing

There should be one obvious way to separate smart / dumb components

This is it for hooks imo:

// hooks container
import PresentationalComponent from 'blah/PresentationalComponent';

/** with hooks, you need to manually create a "container" component */
export default function Container() {
  const props = useReduxState(/* ... */); // not proposing this API btw, just a place holder
  const action = useReduxAction(/* ... */);

  return <PresentationalComponent {...props} onEvent={action} />;
}

Because you have to manually create the container component, it's less obvious that you should separate container and presentational components. For example, some users will probably think, "why not just put useReduxState in the presentational component"?

export default function PresentationalComponent () {
  const props = useReduxState(/* ... */); // not proposing this API btw, just a place holder
  const action = useReduxAction(/* ... */);

  return // ...
}

I still think the separation of container and presentational components is important but I'm not sure it's possible to create an API where we can make it obvious to encourage the separation.

Maybe this is a problem solely docs can solve?

When using custom hooks predictability is an issue on all fronts.
If you see:

const user = useCurrentUser();

in your component, it's not straightforward whether this component is aware of Redux or not, unless you enforce conventions in your team, like:

const user = useCurrentUser();

@adamkleingit Not knowing that the component uses Redux or not is actually better for your business logic design. Redux is an implementation detail. If your hook is called useCurrentUser, the only thing that the hook consumer should rely on is the fact that the current user will be returned. If later on you decide to switch Redux for something else, you only have to work on your custom hooks, and nowhere else.

@markerikson on the related by different topic of releases would it make sense to work one (or all) of these proposals into a 7.0.0-alpha and put it up with a @next tag. Assuming that also included API compatible implementations of connect, connectAdvanced, Provider than it would be possible to use it as a drop-in replacement, and get some real world testing in order to find any lackluster APIs or performance issues.

@chris-pardy : fwiw, my focus right now is coming up with a workable internal implementation that resolves the issues described in #1177, particularly around perf. (At least, my focus outside work. Things at work are really hectic and draining right now, which isn't helping.)

I personally am ignoring the "ship a public hooks API" aspect until we have a 7.0 actually delivered. Please feel free to bikeshed and experiment with actual implementations. Goodness knows there's enough 3rd-party Redux hooks to serve as starting points and comparisons.

I will point out that any assistance folks can offer with the tasks I listed in #1177 will ultimately result in us getting to a public hooks API faster. (hint, hint)

I've just made an example of use store hooks
Codesandbox UseReduxStore hooks

I've tested it on my application and it works well as I see.

useMappedState example
Do we have to change mappedState if mapper function is changed?

export function useMappedState<
    S = any,
    T extends any = any,
    D extends any[] = any[],
>(mapper: (state: S, deps: D) => T, deps?: D): T {
    const depsRef = useRef<D>(deps);
    const mapperRef = useRef<any>(mapper);
    const storeReference = useContext<RefObject<Store<S>>>(ReduxStoreHolderContext);
    const [mappedState, setMappedState] = useState(mapper(storeReference.current.getState(), deps));
    const currentMappedStateRef = useRef<T>(mappedState);

    // Update deps
    useEffect(() => {
        const store = storeReference.current;
        const nextMappedState = mapperRef.current(store.getState(), deps);
        const currentMappedState = currentMappedStateRef.current;

        depsRef.current = deps;

        // Update state with new deps
        if(!shallowEqual(currentMappedState, nextMappedState)) {
            setMappedState(nextMappedState);
            currentMappedStateRef.current = nextMappedState;
        }
    }, [deps]);

    // Update mapper function
    useEffect(() => {
        mapperRef.current = mapper;
    }, [mapper]);

    useEffect(
        () => {
            const store = storeReference.current;

            function onStoreChanged() {
                const nextState = store.getState();
                const nextMappedState = mapperRef.current(nextState, depsRef.current);

                if(!shallowEqual(currentMappedStateRef.current, nextMappedState)) {
                    setMappedState(nextMappedState);
                    currentMappedStateRef.current = nextMappedState;
                }
            }

            return store.subscribe(onStoreChanged);
        },
        [], // prevent calling twice
    );

    return mappedState;
}

useActionCreator example:

export function useActionCreator(actionCreator) {
    const storeReference = useContext<RefObject<Store>>(ReduxStoreHolderContext);

    return useCallback((...args) => {
        storeReference.current.dispatch(actionCreator(...args));
    }, [actionCreator]);
}

Create context to hold store reference

export const ReduxStoreHolderContext = React.createContext(null);

export function ReduxStoreProvider({ store, children }) {
    // Store object isn't changing? So let's pass only reference to it.
    // Don't affect react flow each action
    const storeReference = useRef(store);

    return React.createElement(
        ReduxStoreHolderContext.Provider,
        { value: storeReference },
        children,
    );
}

And backward compatibility connect might looks like

export function connect(mapStateToProps, mapDispatchToProps, mergeProps?, options = {}) {
    const {
        pure = false,
        forwardRef = false,
    } = options;

    return (BaseComponent) => {
        let Connect = function ConnectedComponent(ownProps) {
                const mappedState = useMappedState(mapStateToProps);
                const actionCreators = useActionCreators(mapDispatchToProps);

                const actualProps = useMemo(
                    () => (
                        mergeProps
                            ? mergeProps(mappedState, actionCreators, ownProps)
                            : ({
                                ...ownProps,
                                ...mappedState,
                                ...actionCreators,
                            })
                    ),
                    [ownProps, mappedState, actionCreators],
                );

                return React.createElement(BaseComponent, actualProps);
        };

        if (pure) {
            Connect = React.memo(Connect)
        }

        if (forwardRef) {
            Connect = React.forwardRef(Connect);
        }

        return hoistStatics(Connect, BaseComponent);
    }
}

Regarding smart/dumb components, Dan recently updated his stance on the subject ... https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0, promoting hooks as an equivalent

Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore - from Dan's article

@jbrodriguez oh very interesting. in general, i still think the separation leads to more readable components but I find it fascinating that he doesn't suggest splitting components into presentational and container components anymore.

i think we can use dan's statement to no longer consider "There should be one obvious way to separate smart / dumb components" from his original criteria. doesn't make much sense to consider it anyway i guess?

very interesting and good find

Hey ! I've been working on a package that can help πŸ‘‰ https://github.com/flepretre/use-redux
Initially it was a basic hook implementation of react-redux with the new API context, but recently I've got a recommandation to use react-redux context's so it's easily plug into a existing react-redux app.

This may be a stupid question but would react-redux hooks depend on the new implementation of connect at all or would a hooks API require another implementation? I.e. can you use connectAdvanced to implement useRedux (or similar)?

My thought is no. Is that right?

No, you cannot use HOCs to implement hooks because HOCs wrap components and hooks are just function calls inside components. But as the new react-redux 7.0 alpha uses hooks to implement the HOCs internally it can probably share some of those internals with the hooks API too.

@epeli , @ricokahler : yeah, it's possible that we may be able to extract some of the logic from our v7 implementation of connectAdvanced, turn that into part of the public hooks API, and then reuse it inside of connectAdvanced. Maybe.

Then again, it may also be different enough that there's nothing to reuse.

Either way, though, you wouldn't use connectAdvanced inside the useRedux implementation itself. Function components can contain/use hooks, but hook wouldn't make use of components internally. (Technically you could generate a component type in a hook, but that's not the same as somehow using the component in the hook.)

Since it's hooks API, would it be a good idea if we implement the API in a hooks way?

i.e.: Avoid wrapping the component by sending the store data to React component state directly rather than via props.

It will be something looks like the following:

function useReactRedux(props, options = {}) {
    const { initState, stateSelector } = options;
    const [state, setState] = useState(initState);
    const context = useContext(ReactReduxContext);
    const contextToUse = useMemo(() => {
        const propsContext = props.context;
        return propsContext &&
            propsContext.Consumer &&
            isContextConsumer(<propsContext.Consumer />)
            ? propsContext
            : context;
    }, [props.context, context]);
    const store = contextToUse.store;
    /**
     * Setup store subscription
     * Will unsubscribe when component unmount
     */
    useEffect(() => {
        return store.subscribe(() => {
            const newState = stateSelector(store.getState());
            // --- only update when state changes
            if (newState === state) return;
            setState(newState);
        });
    }, []);
    return [state, store.dispatch];
}

// --- When use the this API, we can:

function MyComponent(props) {
    const [state, dispatch] = useReactRedux(props, {
        initState: {
            count: 0
        },
        stateSelector: state => state.myComponentState
    });
    return (
        <div>
            Count: {state.count}
            <button onClick={() => dispatch({ type: "CLICK" })}>Click</button>
        </div>
    );
}

Benefits:

  • Since we are not passing store data via props, we won't need to mapStateToProps. Consequently, we don't need to worry about the zombie child component issue (mentioned in #1177 ) as there won't be any stale props
  • No extra wrapping component --- more Hooks way
  • No need to handle forwardedRef & wrapperProps etc. as there is no extra higher order component. User can handle those directly if they need. Make react-redux function more pure & clean

I used the same approach to implement my own library fractal-component and here is the hook API implmentation useComponentManager.

Potentially, the similar approach could work for class components as well (so that we can have consistent way of connecting components).
e.g.

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        /**
         * We can't use React Hooks in Class Components
         * But we can manually retrieve context & setup subscription 
         * at right timing via lifecycle methods
         */
        connectToRedux(this, options);
    }
    render(){
        ...
    }
}

@markerikson I understand that this likely premature given that v7 beta was just released, but do you have any thoughts on a timeline for when the public hooks API might conservatively be released? I ask because I am starting a new application and I'm trying to decide if I should delay the react-redux aspects of it or if I should continue full speed ahead and refactor later. I'd like to avoid some churn this early on if possible. Thanks! And thank you for all of the amazing work on this project.

@klandell : no timeline yet. To be entirely transparent:

  • v7 going final is an absolute pre-requisite for even starting to consider a public hooks API
  • Given that v7 is coming along relatively soon after v6, I am absolutely determined to make 100% sure that it's rock-solid before I hit the button to publish v7-final
  • We've gotten along until now without hooks, and everyone's using connect right now. We need to make sure that works right as our highest priority. Hooks can wait a bit longer.
  • All my free mental time and energy that I actually had to devote to OSS work has been focused on v7 the last couple months
  • At the same time, work has been extremely busy and stressful over these last couple months as well, so there's been a lot of nights where I simply didn't have the mental capacity to do anything "productive"
  • I basically have done v7 entirely by myself. I've gotten some bits of feedback from folks, but it's effectively been a one-man show development-wise.
  • I've got a couple business travel trips coming up, plus at least one other potential trip this summer
  • There's a ton of bikeshedding that's going to be involved in figuring out what hooks APIs we actually want to ship, and then technical work to actually come up with them, and then more beta testing and stuff.

So, frankly, I don't see it happening very soon, and I don't even want to speculate on actual timelines.

@markerikson Refactor later it is then. Thanks again!

@klandell My personal plan to use hooks now is just to use my own redux bindings for now which exposes an hooks api. The idea is to port it to use the official bindings once it ships public hooks primitives which allows me do it. So I won't be forced to refactor anything if want to use something from react-redux in future. Also it would be foolish to not use react-redux when it is possible: The work @markerikson has done with it is highly impressive related to the edge case handling and performance.

The caveat in this is currently that it should not be used in projects that already uses react-redux and connect() because they implement store subscriptions bit differently which can cause subtle bugs if data from connect() is mixed with my useMapState(). I documented here how I went about it.

@epeli : yeah, "top-down updates" and mixing different subscription approaches is one of the biggest concerns I have about a potential hooks API.

To answer your footnote from the linked issue: batchedUpdates is insufficient for correctness purposes for us because the subscription callbacks run immediately, thus referencing whatever the previous saved wrapperProps are. It's entirely likely that this store update would cause parent components to update and thus provide new wrapper props, or possibly even stop rendering this component. We can't know that for sure until the nearest connected ancestor has committed its render, because that means all unconnected components between it and this one have rendered too and therefore we have the latest props accessible.

batchedUpdates only ensures that all queued updates in the same tick get handled in one render pass. We need to delay running mapState until after those renders are done, basically.

Ignore this sorry.

@markerikson Can you explain to me why top-down updates is necessary per se? I understand how the zombie bug happens (thanks to your great descriptions) and I see how that's an unintuitive thing to run into.

However, now I'm wondering if top-down updates are actually required to have working code or if it's possible to program without the assumption of top-down updates (e.g. check and early return if props are stale).

The reason I ask is because I can't really think of clean way to enforce top-down updates and I'm wondering if it's worth the effort in enforcing it.

(AFAIK,) The way top-down updates were enforced previously was via the connect HOC adding a sort of subscription layer by wrapping the components below it with another provider. The HOC gives a nice and clean way to add the subscription layers and I don't know how to do that solely hooks.

So that leads me to the question: Do we really need it?

What do you all think? Am I missing something?

Ignore this sorry.

To backtrack a bit, I have this dream of ridiculously simple redux hooks where the convention is that they should only be selections with minimal calculations. e.g.

import _get from 'lodash/get';
import { useMemo } from 'react';
import { useRedux } from 'react-redux';

import PresentationalComponent from './PresentationalComponent';

function Container({ id }) {
  const selection = useRedux(state => state.subStore.something, []);
  const otherSelection = useRedux(state => state.otherStore.something, []);

  const joinedData = useMemo(() => {
    const displayName = _get(selection, [id, 'displayName']);
    const title = _get(otherSelection, [id, 'title']);
    return { displayName, title };
  }, [id, selection, otherSelection]);

  return <PresentationalComponent joinedData={joinedData} />;
}
  • The first thing I like about this API is that it's very simple and communicates what's going in the simplest way possible. I bounced this idea off all the engineers on my team at all different levels of seniority and they all knew what was going on. I also like the aesthetic of making a hook literally called useRedux because it's very clear that it's the thing that makes pushes happen from redux.
  • The second thing I like about this API is that it leads to relying on React for optimizations and makes react-redux do a less work. Instead of relying on connect or reselect to memoize, the convention would be to useMemo etc.

The issue with the above comes with how something like useRedux would be implemented. Thinking about it naively, every useRedux call could amount to one subscription to the redux store which doesn't scale very well. E.g. if there are 1000+ subscriptions then each unsubscribe would take O(n) time.

To reduce the amount of redux subscriptions, I have this somewhat crazy idea of batching updates via selection so that if two or three different components (no matter where in the tree) select the same pointer, then they'll create only one redux subscription and we can also use ReactDOM.unstable_batchedUpdates to further speed up updates.

For example:

function Foo() {
  const selection = useRedux(state => state.foo.a, []);

  // ...
}

function Bar() {
  const selection = useRedux(state => state.foo.a, []);

  // ...
}

function Baz() {
  const selection = useRedux(state => state.foo.a, []);

  // ...
}

Behinds the scenes, useRedux would see that each Foo, Bar, and Baz all selected the same pointer so it can assume that they can be batched into the same redux subscription.

This requires:

  • the assumption that no object is placed in the redux tree more than once (which i feel like is a fine assumption given how people use redux).
  • the optimization will only work if you select a reference type (e.g. selecting a primitive or string wouldn't work)
  • ⚠️ no longer having the ability to rely on top-down order updates (which started the comment above)

I just think redux hooks should be simple and easier to understand than connect. I think make a simple hook API will lead to better conventions (e.g. making custom hooks for joining data with props).

I also think it echos the philosophy of hooks too. Hooks exist to create a simpler state primitive so maybe a simpler redux subscription primitive would be better too.

Let me know what you think!

Top down order updates are needed because if a child updates before a parent, the same child might receive outdated props from a connected parent after it has already rendered.

@ricokahler : first, it's worth going back and reviewing the original design constraints for the React-Redux connect API (which I quoted earlier in this thread).

In particular, these two constraints are highly relevant:

  • Smart components' select function needs to be able to take their props into account
  • We should have shouldComponentUpdate wherever we can

In other words:

  • What data you read from the store may frequently depend on what props you have, like <ConnectedListItem id={123} />
  • For performance reasons, we only want the component to re-render if its props have actually changed.

It's certainly possible to create a hook that reads from a larger chunk of the store, and then lets you handle pulling out a specific piece based on props in your component's render logic. You can mimic that right now:

const mapState = (state) => {
    return {todos: state.entities.Todo}
}

render() {
    const {todos, todoId} = this.props;
    
    const todo = todos[todoId];
    // render it
}

The issue is that will cause this component to re-render unnecessarily when any other todo is updated, thus causing perf overhead. The same would be true in a hook that just extracts that bit of state and calls setState(todos).

Now, it's possible that the use of unstable_batchedUpdates() would lessen the impact of that somewhat, but based on what I've seen with v5, v6, and v7, the best approach for minimizing perf impact is to only force a re-render when we know that the exact state (extracted and derived) that this component needs has actually changed. Getting React involved on every store update is simply more expensive, period. That's why v5 and v7 are faster than v4 and v6.

The next issue is potentially mixing connect and useRedux. If connected components are updating in one sequence, and components calling useRedux update in a different sequence, that's going to cause both unnecessary re-renders and inconsistent behavior.

It's hypothetically possible that we could somehow come up with a way to enforce top-down updates by manipulating subscriber ordering based on the tree structure. At that point, though, you're actually sort of reinventing React's reconciliation algorithm :)

Top down order updates are needed because if a child updates before a parent, the same child might receive outdated props from a connected parent after it has already rendered.

@saboya yeah this all makes sense now. ignore my previous "does order really matter?" thing.


I'm still for the useRedux API though. I think if we can get that API to be performant, that's the way to go.

Smart components' select function needs to be able to take their props into account

I think I implied that useRedux couldn't accept props but that's the reason for the second argument of the dependencies array useRedux(state => /* ... */, []) // πŸ‘ˆ.

Without the "no order guarantee thing", this is actually the same API as @epeli's useMapState and @Jessidhia's useSelector, just renamed to useRedux because I like the aesthetic.


@markerikson I'm spit-balling here so I apologize if things don't make sense but maybe we could have an overload for connect that would enable useRedux with top-down updates?

So the new "container" could look like this:

import { connect, useRedux } from 'react-redux';
import PresentationalComponent from './PresentationalComponent';

function Container({ id }) {
  const todo = useRedux(state => state.todos[id], [id]);
  return <PresentationalComponent todo={todo} />;
}

export default connect(Container);

Throwing the HOC back into the mix would allow for another Provider to wrap the Container and then useRedux can pick up on the new context.

Maybe something like that would work??

maybe we could have an overload for connect?

OH PLEASE NO NO MORE OVERLOADS FOR CONNECT AHHHHHHHH

runs away screaming

So asking a more serious question:

  • How do any of the other "unofficial" Redux hooks libs handle working with component props?
  • How critical is it to be able to use component props as part of your state extraction logic?

Not sure I follow, what do you mean by "component props"? How would you extract specific parts of your store if not by using your props to cherry-pick from your state?

@saboya : yes, exactly.

In other words, how do other Redux hook libs translate this:

const mapState = (state, ownProps) => {
    return {
        todo : state.todos[ownProps.id]
    }
}

into hooks form?

@markerikson

How critical is it to be able to use component props as part of your state extraction logic?

I think using component props as part of your state extraction logic is not the root cause to the zombie child problem.

Connecting Redux store data to component via Component Props is the actual culprit.

i.e. If you want BOTH using component props as part of your state extraction logic & Connecting Redux store data to component via Component Props, you will have to provide an API like:

const mapState = (state, ownProps) => {
    return {
        todo : state.todos[ownProps.id]
    }
}

Keep in mind that the Component Props are only up to date during rendering staging (i.e. when render() function is called or function component is rendered), you will always have to deal with outdated Props.

On the other hand, if we connect Redux store data to component via Component State (as I suggested in my post), we have still have the using component props as part of your state extraction logic feature. However, it will not provided as part of the API and be done at rendering staging.
i.e.:
We only need provide an API like:

const mapState = (state) => {
    return state.todos
}

And the API user can still use component props as part of your state extraction logic by:

function MyToDo(props) {
  const todos = useRedux(mapState);
  // As it's in render stage now, `props.id` is guaranteed to be an up to dated one 
  return <PresentationalComponent todo={todos[props.id]} />;
}

Consequently, the following

How do any of the other "unofficial" Redux hooks libs handle working with component props?

won't be a problem now as it's React's reconciliation algorithm makes sure

 return <PresentationalComponent todo={todos[props.id]} />;

never cause issues (either never be reached / called due to component unmount or only passing up to date props when render)

@t83714 : as I said a few comments ago, the problem is that wouldn't be good for performance. This component would be forced to re-render any time state.todos changed, not just when state.todos.123 changed.

I'm not saying that makes the approach impossible, just pointing out that it's likely to lead to worse performance overall. That does make it a less likely candidate approach.

@markerikson

Thanks for pointing out the performance issue.

Understood that's a hard decision to make but thought solving the performance issue might be easier than fix the top down update issue as:

  • there are more tool available to handle the performance issue (e.g. PureComponent or React.memo)
  • a common problem --- we have deal with it everyday and somehow even if we go with Connect Redux via Component Props apporach as we always have to call setState to trigger the rendering

Could we fix the simple example above by:

function MyToDo(props) {
  const todos = useRedux(mapState);
  // As it's in render stage now, `props.id` is guaranteed to be an up to dated one 
  const PurePresentationalComponent= React.memo(PresentationalComponent);
  return <PurePresentationalComponent todo={todos[props.id]} />;
}

I mean, probably, it should be something very common and up to user to fix within user space?

If we really want to include it as part of the API (although I think it should be user's job to handle it), nothing stop us to provide API like:

function MyToDo(props) {
  return useRedux(mapState, todos => {
      return <PurePresentationalComponent todo={todos[props.id]} />;
  }, true);
  // --- here `true` indicates a Pure component and by default should be false
}

@t83714 : the immediate issues with those suggestions are :

  • for best perf, we need to avoid forcing a re-render if the data hasn't changed
  • we actually can't rely on useState or useReducer bailing out of the new value is same as before, because that runs in the render phase and therefore requires a re-render even if there's no change.
  • PureComponent and React.memo don't help here, because this is inside the component, and those work in props coming from outside

And the difference with what connect does now is that we only force a re-render if the derived value has changed.

I still desperately wish that the v6 approach had worked out. If we were getting the store value from context, none of this "stale props" stuff would be an issue at all. Unfortunately, the combo of perf issues and inability to bail out of context updates made that turn out to be a dead end for now.

I do agree that this is the perfect time to reevaluate things and maybe come up with a very different API. But, it's also true that the current API exists because these are exactly the kinds of things people want to do.

@markerikson

Thanks a lot for your feedback.

I didn't mean to avoid re-rendering the MyToDo component. I was trying to stop the re-rendering of
component PresentationalComponent.

The shallow comparison result of todos[props.id] should be unchanged. Thus, component PresentationalComponent won't be rendered, right?

Probably only the only mistake I made in my example was that creating a Pure version of the component should be outside the function component (or using the React hook API):

// create a Pure version of Component. This probably should be the user's job
const PurePresentationalComponent= React.memo(PresentationalComponent);

function MyToDo(props) {
  const todos = useRedux(mapState);
  // As it's in render stage now, `props.id` is guaranteed to be an up to dated one 
  return <PurePresentationalComponent todo={todos[props.id]} />;
}

Can I confirm whether it would still be a performance hit if we can avoid re-rendering PresentationalComponent?

Just re-rendering MyToDo component seems won't create too much work as it leads to no new work to the commit phrase (because MyToDo returns the same tree (PresentationalComponent) that doesn't require re-render)?

@markerikson

So asking a more serious question:

* How do any of the other "unofficial" Redux hooks libs handle working with component props?

* How critical is it to be able to use component props as part of your state extraction logic?

It's very critical.

I create components like this with @epeli/redux-hooks all the time

function Avatar(props) {
	const user = useMapState(state => state.users[props.userId]);
	return <img src={user.avatarURL} />;
}

No issues with tearing so far that I known of.

@t83714 Here's how I handle the perf issues if you are interested in a working implementation: https://github.com/epeli/redux-hooks/blob/master/docs/optimizing.md No need to use pure components or React.memo()

So, as @markerikson knows, I have been thinking about this quite a lot recently (he had to suffer through my ramblings in the other issue ;) )

Most of the solutions I see posted in here still will suffer from the zombie child problem.

@t83714 Your solution also won't work unconditionally, since your assumption that in the render phase all props are up to date is only true if the update that causes the re-render is using batching, since otherwise the setState call inside the store subscription callback will cause an immediate re-render of the component before the parent component could handle the store update and update the props. I think it will be easier to see this in action yourself (see the console output). I slightly adjusted your code, but the idea is still the same. As you can see, the invariant should be that the count from the props is always the store count + 1, but without batching you see an inconsistent render. With batching (i.e. useProxy = true inside App) it works correctly.

With all this being said, react-redux is already using batching for the tiered subscription approach, so for your suggestion to work it just needs to be ensured that the store the hook is subscribing to is also using batching for notifying subscribers. Maybe you already took this into account, when writing your suggestion. If so, I apologize.

In response to this

Just re-rendering MyToDo component seems won't create too much work as it leads to no new work to the commit phrase (because MyToDo returns the same tree (PresentationalComponent) that doesn't require re-render)?

You are right, that only MyToDo's render executes which the children being memoized. However, I remember reading somewhere (can't find it right now sadly) that in terms of performance impact the render phase is actually the expensive one, not the commit phase (since behind the scenes much more than just the function call to MyToDo happens during render). That means if you have many many components using the hook, you will still feel the performance impact.

@epeli Indeed, your solution does not suffer from the classical zombie child problem. However, it is easily possible to show that in your version sometimes the mapState is called with inconsistent props and state. This is still a stale props issue that can cause the mapState to throw.

@MrWolfZ Damn. I have a test for this case. Not sure why it passes actually now... But I probably need implement similiar mapState delaying as react-redux does. Thanks for the heads up!

@epeli your version of hooks is really similar to how I imagined it could look like. My idea for fixing the stale props issue was to just catch all errors when calling mapState in the subscription callback, and then just force a re-render if an error was caught. This will cause the mapState to be executed again and throw again (if the issue still persists), or return the correct value. In theory this should work, but I feel dirty just swallowing errors like that.

Also, it has timing issues that make this impossible with concurrent mode, but no hooks solution I saw so far is compatible with that anyways due to the way the store is accessed.

Alright, I've been doing some soul searching, I've finally read the v7 code, and I think I have a general direction for how I think these redux hooks thingies should work:

  1. I believe that useRedux(selectorFn, dependencies) is the simplest and most versatile API. It's more primitive than some other suggestions but I think it'll serve as the best building block for custom hooks that useRedux (and look how cool that pun is!!). Also, many of the suggestions above included this API so I think it's an API most people would expect. We can include others, but useRedux seems to be the best building block and starting point.
  2. Given that API, there is still the constraint of top-down updates and making those top-down updates work with connect. The only way I can foresee this being possible is by re-using the same tiered Subscriptions that connect uses.
  3. connect propagates those subscriptions using context by wrapping the wrapped component's children with another Provider. The way I see, the hooks version of connect will still require an HOC but this one doesn't need to contain any parameters. It'll look something like this:
// `enableReduxHooks` is a placeholder function name
import { enableReduxHooks, useRedux } from 'react-redux';

function Todo({ id }) {
  const todo = useRedux(state => state.todos[id], [id]);
  return // ...
}

// still need to wrap in order to propagate context and tiered subscriptions
export default enableReduxHooks(Todo);
  1. Given that users may want to useRedux more than once in the function component, it makes sense that the context that enableReduxHooks provides will create one redux subscription and all the hooks on that level would share that subscription.
  2. It also makes sense to use batched updates to make multiple calls to useRedux run in one tick.

Remarks:

  • Still requiring an HOC to make hooks work is a bit annoying but I think given the constraints, this is the best way to get useRedux and connect to play nicely together since we're using the same tiered subscription mechanism.
  • Thinking about it more the enableReduxHooks wouldn't actually affect how people use hooks. People can still create custom hooks using useRedux and not have to think about enableReduxHooks. e.g.
// this custom hook isn't aware of `enableReduxHooks` and everything is fine
function useTodo(todoId) {
  const todo = useRedux(state => state.todos[todoId], [todoId]);
  return todo;
}

const Todo({ id }) {
  const todo = useTodo(id);
  return <div>{/* ... */}</div>
}

export default enableReduxHooks(Todo);
  • We could possibly even create a linter that follows the useRedux calls and ensures the closest component wraps with enableReduxHooks. To make adding the HOC less of a thought.
  • enableReduxHooks is not the best name in my opinion. I originally thought overloading connect would be cool but that made @markerikson run away screaming πŸ˜…. Please come up with a better name.

What does everyone think about that approach?

Vote with the πŸ‘ or πŸ‘Žreactions.

@markerikson I'd be willing to work on a PR off the v7 branch with this approach if you're open to the idea. I wouldn't expect you to pull it in immediately or consider it too seriously. At the very least, I think it would start a good topic of discussion. What do you think?

@ricokahler : Please, go ahead! I'd absolutely welcome proof-of-concept PRs for discussion, especially now that we have a better idea what some of the constraints are.

The discussions over the last couple days have gotten my brain to start chewing on this a bit. No specific ideas atm, but thoughts are at least circulating.

I would really like to figure out something that doesn't require use of a HOC at all. I don't know what that might be atm or if it's even feasible, but I assume most hooks users are looking to drop HOCs entirely.

MrLoh commented

I think a dependencies array is the obvious way to deal with props, it’s a well known pattern in hooks land and minimal effort, especially with the lint rule. Redux is all about being more explicit over being most succinct anyway.

Needing to wrap components to use hooks is obviously extremely unintuitive. Do none of the other libs have a better solution to this issue? Since it only is needed to handle mixing and matching connect and hooks, there should be a way to opt out for projects that will not mix and match, then it sounds like a workable intermediate workaround.

Did some reading on the v7 code too and came to pretty much to the same conclusion as @ricokahler: It doesn't seem like it's possible to implement hooks without some kind of wrapping component unless React itself would come forward and give us something like

  • mapState directly on useContext
  • create context providers using hooks only
  • allow to read the component tree efficienctly so we could know which order to update the listening hooks

My hooks implementation is indeed way too naive.

My hooks implementation is indeed way too naive.

@epeli If it makes you feel better, I have an even more naive useRedux implementation in prod right now lol


I agree with everyone that the hooks still requiring the HOC is annoying/gross but I think in practice, it won't change how people use the redux hooks. See this example pasted from my previous post:

// this custom hook isn't aware of `enableReduxHooks` and everything is fine
function useTodo(todoId) {
  const todo = useRedux(state => state.todos[todoId], [todoId]);
  return todo;
}

const Todo({ id }) {
  const todo = useTodo(id);
  return <div>{/* ... */}</div>
}

export default enableReduxHooks(Todo);

I would also like to figure out a way to do it without a wrapper but I don't think it's possible.

For those who haven't read the code: the only reason we need the wrapper is to add that subscription layer to enable top-down updates. See here from the v7 branch:

// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)

This Provider propagates the Subscription instances to the next component wrapped in connect:

// This Subscription's source should match where store came from: props vs. context. A component
// connected to the store via props shouldn't use subscription from context, or vice versa.
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)

By the nature of HOCs, it's easy to wrap the component with another provider seamlessly but with hooks, there is no way to do that 😭


After I figured that out, I admitted defeat and started seriously thinking about how to implement useRedux (aka useMapState, useSelector) with the constraint of the wrapper and it's actually kind of nice from an implementation standpoint.

Earlier in the thread, @Jessidhia brought up the same API as useRedux just named useSelector and she said:

[The useSelector API] would also have the side-effect of creating a separate subscription per useSelector, though. I don't know if that's a relevant performance consideration or not.

With the wrapper, instead of having one redux subscription per useRedux (aka useSelector) call, it would be one redux subscription per enableReduxHooks call.

Internally the enableReduxHooks wrapper could create one subscription and manage pushes to each useRedux call inside that "subscription layer" (where each enableReduxHooks call is one layer).

I think this would bring performance on par to how connect works now because this would make it work like how connect works.


It's unfortunate and gross that the HOC is still needed but with this constraint there is a clear path to fully function redux hooks that work alongside with connect πŸŽ‰ (in my mind at least)

Maybe I'm not thinking outside of the box enough but if we can figure out how to get the tiered subscriptions (e.g., the "order matters") without the wrapper then we can ditch the HOC.

@ricokahler your posts reflect my own thoughts almost exactly. I also got to the conclusion that currently the HOC will still be required. Instead of enableReduxHooks my idea was to just call it connectHooks to stay in line with existing naming.

I think this code (basically your example with some inlining) is quite concise and readable.

export const Todo = connectHooks(({ id }) => {
  const todo = useRedux(state => state.todos[id], [id]);
  return <div>{/* ... */}</div>
})

Regarding the alternative approach of "enforcing" that no props are used for selecting the state (which would only be possible with linters as far as I can see), I think it would definitely be worthwhile to do some performance tests to see how big the impact of those additional renders really is. I have some free time on my hands the next couple of weeks, so I may try my hand at it.

Lastly, I also plan to take a deep dive into the fiber implementation. Maybe I'll find some hidden nugget that would allow us to have a proper hooks-only implementation.

Giving up the possibility of filtering state with props is not an option in my opinion. Maybe for some, but for some cases it's going to cause a huge amount of unnecessary renders.

I think the hybrid approach (HOC + hooks) is the way to go for now. The main issue with the HOC isn't the HOC itself IMO, but the code clutter and ergonomics. And, if in the future there's a HOC-less solution, the migration path will be almost pain-free, and the HOC can be updated to a NOOP.

I really don't like the idea of having to use a HOC just to use hooks.

Unfortunately I do agree that it looks like it may be a necessity atm, but I would really like to try to come up with another option.

The vague notion I'm tossing around in my head is if there's some way we can construct our own in-memory tree structure in <Provider> that mirrors the shape of the store subscriptions in the React tree, so that we can trigger updates in the appropriate sequence. Problem is, we don't actually know where each component is in the tree.

If y'all have any ideas on how to pull off such a feat, I'm all ears.

@saboya One of the suggestions above was something like the following code, which would put some burden on the library users to always remember to use useMemo, but should keep performance problems minimal due to not everything being re-rendered (again, only tests with concrete numbers will verify or contradict this).

export function Todo({ id }) {
  const todos = useRedux(state => state.todos);
  const todo = todos[id]
  return useMemo(() => (
    <div>{/* ... */}</div>
  ), [todo])
})

@MrWolfZ : the question of how many components render is only part of the issue.

In that snippet, yes, the child components memoized by the useMemo() would only re-render when todo changes, because React will bail out when it sees the === elements in this spot as last time.

However, the Todo component itself will still have to render even if todo hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.

Now, we ought to try building these options out, and then updating the benchmarks suite to have some hook scenarios for comparison with our existing HOC scenarios. (No idea how that will work out).

I don't think using dependencies to useRedux is a good idea, suppose we use redux like:

const User = ({id}) => {
    const user = useRedux(
        state => state.users[id],
        [id]
    );

    return <div>{user.name}</div>;
};

It looks very concise and readable by specifying id as a dependency to mapState function, however this implies a fact that the selector won't change around renders, which is not a stable consumption:

const User = ({userType, id}) => {
    const selectUser = userType === 'admin'
        ? state => state.admins[id]
        : state => state.users[id];
    const user = useRedux(selectUser, [id]);

    return <div>{user.name}</div>;
};

User won't get correct result from redux if userType is changed without id change, and lint rules like exhaustive-deps is not able to detect the fact that selectUser actually depends on userType variable.

We have to tell developers to modify this example code to:

const User = ({userType, id}) => {
    const selectUser = state => userType === 'admin' ? state.admins[id] : state.users[id];
    const user = useRedux(selectUser, [userType, id]);

    return <div>{user.name}</div>;
};

This may be OK in a self controlled app but is not the case as a general purpose library like react-redux, we should try to satisfy all kinds of developers without having implicit restrictions.

I'd prefer to manage my selector via useCallback myself:

const User = ({userType, id}) => {
    const selectUser = useCallback(
        userType === 'admin'
            ? state => state.admins[id]
            : state => state.users[id],
        [userType, id]
    );
    const user = useRedux(selectUser);

    return <div>{user.name}</div>;
};

This is more straightforward and is able to utilize exhaustive-deps lint rules

Unfortunately I do agree that it looks like it may be a necessity atm, but I would really like to try to come up with another option.

@markerikson a babel plugin? idk

last time I checked. create-react-app didn't play nicely with those though


Edit: there are these things called babel macros that work with create-react-app. I'm not sure what they do exactly but maybe we can babel some HOCs in magically.

Edit edit: In order to use babel macros, the importing line has to match /[./]macro(\.js)?$/.

Is that cool or gross? import useRedux from 'react-redux/macro' is gross.

Eh that's about as far as I'm willing to go for considering babeling enableReduxHooks.

MrLoh commented

@otakustay this is a bad example. Just like with any hook, functions also need to be specified as dependencies, so if you pass selectUser as a reference, then you need to specify it as a dependency and the exhaustive-deps rule would also capture that missing dependency on the function in useMemo or useCallback. In your example id is actually not a dependency of that hook but just the function is. See https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies and also https://overreacted.io/a-complete-guide-to-useeffect/

@MrLoh

Just like with any hook, functions also need to be specified as dependencies

Except useCallback. What I'm discussing is actually "should useRedux includes the capability of useCallback", as a result I said NO, we should just keep selector function as a dependency of useRedux

@MrWolfZ

Maybe you already took this into account, when writing your suggestion. If so, I apologize.

Thanks for the sample code. I noticed React-Redux's Subscription implementation but I think we can avoid that by connecting redux via state πŸ˜„

My statement all props are up was talking about one component.

I think why the zombie child component is unintuitive & really need a fix is because that's the situation where data of the SAME component are consistent.

i.e. the stale props (props.id) and state (todos[props.id]) of the same component could be not synced (at a point in time)

The case of your sample code is two components' internal state could be not synced at the same time.

I think we probably shouldn't need to worry about this case too much as you probably hardly see it in a real-life example? --- If you subscribe to the same state, there would be no point of passing the state via props. And if you don't pass the state data to the child component via props, it will be no chance that two copies of states exist in the SAME component.

@markerikson Regarding this comment

@MrWolfZ : the question of how many components render is only part of the issue.

In that snippet, yes, the child components memoized by the useMemo() would only re-render when todo changes, because React will bail out when it sees the === elements in this spot as last time.

However, the Todo component itself will still have to render even if todo hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.

Now, we ought to try building these options out, and then updating the benchmarks suite to have some hook scenarios for comparison with our existing HOC scenarios. (No idea how that will work out).

I found an interesting statement in the official react dooks regarding useReducer having a similar behaviour. Let me quote it here:

If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go β€œdeeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

So they seem to think that having that single component re-render without any children also re-rendering shouldn't be too much of an issue.

That said, I completely agree that both API designs I posted are ugly and should be prevented if possible.

@t83714 if you look at the example @markerikson gives for the zombie child problem, you will find a pattern that is likely to occur in real applications (otherwise, how would they have noticed the zombie child problem in the first place?).

Sorry for the spam, but just for completeness' sake, here my proposed hooks implementation that requires neither a HOC nor use of useMemo and does not suffer from zombie children, but has the downside of calling the state selector with inconsistent props and also calling it on each render and twice for each relevant store change. You can see it in action here with the zombie child example from @markerikson. Of course it is not fully performance optimized yet and I am sure I am missing some edge cases, but this should demonstrate the idea of just ignoring errors during the subscription callback.

import { ReactReduxContext } from "react-redux";
import { useContext, useReducer, useRef, useEffect } from "react";

export function useRedux(selector, deps) {
  const { store } = useContext(ReactReduxContext);
  const [_, forceRender] = useReducer(s => s + 1, 0);

  const latestSelector = useRef(selector);
  const selectedState = selector(store.getState());
  const latestSelectedState = useRef(selectedState);

  useEffect(() => {
    latestSelector.current = selector;
  }, deps);

  useEffect(
    () => {
      function checkForUpdates() {
        const storeState = store.getState();
        try {
          const newSelectedState = latestSelector.current(storeState);

          if (newSelectedState === latestSelectedState.current) {
            return;
          }

          latestSelectedState.current = newSelectedState;
        } catch {
          // we ignore all errors here, since when the component
          // is re-rendered, the selector is called again, and
          // will throw again, if neither props nor store state
          // changed
        }

        forceRender();
      }

      checkForUpdates();

      return store.subscribe(checkForUpdates);
    },
    [store]
  );

  return selectedState;
}

@MrWolfZ
Thanks for the example link. I used a much simple array based sample app to reproduce the problem πŸ˜„
I think you're right. As long as we set up separate subscriptions for components, it's unavoidable unless we use batchUpdates or custom subscription logic to enforce top-down updates πŸ‘

  1. Do not use whole Store context changes to update components. It's not a good performance. Latest context API is pretty slow for searching child subscribed components.
  2. We do not need the whole store state for each component. Let's use particular selectors to select what appropriate component needs.
  3. Do not update components if the reference to selector is changed. A function might be recreated by accident but there is not a case to change input selector.
  4. If we need any props as dependencies then the better approach is using an array of deps because comparing objects looks slower. useStoreSelector((state, [a, b]) => selectSome(state, a + b), [a, b]).
  5. Using HOC to reduce subscribe count is pretty unnatural. Let's use linter rules for it. A developer has to keep in mind some simple things.

How critical is it to be able to use component props as part of your state extraction logic?

Here are my two cents on this ^ discussion point:

@markerikson said

However, the Todo component itself will still have to render even if todo hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.

I agree with this. If you have 100 or so connected components in a list and all 100 of those update, that's still 100 updates regardless if those updates are fast or not. I acknowledge that you can useMemo to speed up all 100 of those updates… but it's still 100 updates.

For example, if you have 100 <Todo />s in a list and you allow the user to edit the title of the todo using an <input />, then you will feel the 100 updates with every keystroke. I've been in that situation before and I've learned my lesson. From my experience regarding components, it's never a good idea to turn an O(1) problem into an O(n) problem if you don't have to.


However, @gaearon in this facebook incubator issue has also gave the notion of an API that doesn't take in props.

Here is a full quote of his post:

To be clear I'm not suggesting this particular API. Just that the notion of running a selector that involves props to determine a bailout has a lot of pitfalls (such as handling stale props), and can cause other issues in concurrent mode (such as the current code which mutates refs during rendering). It doesn't really map to the React conceptual model nicely. From React's point of view, props don't really exist until you render β€” but this Hook is trying to "bail out" at arbitrary times outside rendering.

One alternative design could be to allow selectors but only static ones:

import {createSelector} from 'react-redux-hook'

const useTodosLength = createSelector(state => state.allTodos.length)

function Todos() {
  const todosLength = useTodosLength()
  return <h1>Total: {todosLength}</h1>
}

If you need to select based on a prop value, you can do this with a component layer:

import {createSelector} from 'react-redux-hook'

const useTodos = createSelector(state => state.allTodos)

function Todos() {
  const todos = useTodos()
  return todos.map(todo => <Todo {...todo} />)
}

const Todo = React.memo(() => {
  // ...
})

Essentially, in this case the selecting component is your mapStateToProps. :-)

His post echos what @t83714 had said previously just lifting the selector out of the actually hook.

From an API standpoint, the static creatorSelector makes sense if you don't want to include props but in general, I still think using props to select a specific piece of state is necessary to make things scale.

BUT theory is theory. Until we build it out and benchmark it, there's no way to know. Maybe something like createSelector is the way to go? I'm skeptical but always open to different ideas (e.g. I'm still thinking about babeling that HOC in).

I finally have some numbers for y'all. I implemented two alternatives for useRedux which you can see here. Alternative 1 is my proposed implementation from above with some slight adjustments. Alternative 2 is the approach of a static selector and useMemo.

To benchmark this I have created a copy of the deeptree benchmark for each alternative and made some minor adjustments for each alternative. For alternative 1 I replaced ConnectedCounter with this:

const Counter = ({ idx }) => {
  const value = useReduxAlternative1(s => s.counters[idx], [idx])
  return <div>Value: {value}</div>
};

For alternative 2 I replaced ConnectedCounter with this:

const selector = s => s.counters;
const Counter = ({ idx }) => {
  const counters = useReduxAlternative2(selector)
  const value = counters[idx]
  return useMemo(() => <div>Value: {value}</div>, [value])
};

Below you can find the results. Assuming I have correctly implemented everything, alternative 1 is on par with 7.0.0-beta.0. However, alternative 2 is massively slower (much slower than I expected).

@markerikson Which of the benchmarks do you think would be best suited to test the hooks implementations? I don't really want to re-implement all of them for all alternatives, so it would be great to know which your preferred one is (e.g. because it reflects real world usage best).

Results for benchmark deeptree:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Version      β”‚ Avg FPS β”‚ Render       β”‚ Scripting β”‚ Rendering β”‚ Painting β”‚ FPS Values                                      β”‚
β”‚              β”‚         β”‚ (Mount, Avg) β”‚           β”‚           β”‚          β”‚                                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 5.1.1        β”‚ 14.55   β”‚ 109.9, 0.1   β”‚ 6466.00   β”‚ 8835.47   β”‚ 3001.32  β”‚ 14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,14 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 7.0.0-beta.0 β”‚ 24.11   β”‚ 111.8, 0.8   β”‚ 337.77    β”‚ 13320.03  β”‚ 4784.63  β”‚ 24,25,24,23,24,25,24,24                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Results for benchmark deeptree-useReduxAlternative1:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Version     β”‚ Avg FPS β”‚ Render       β”‚ Scripting β”‚ Rendering β”‚ Painting β”‚ FPS Values                                   β”‚
β”‚             β”‚         β”‚ (Mount, Avg) β”‚           β”‚           β”‚          β”‚                                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 7.0.0-hooks β”‚ 24.05   β”‚ 111.7, 0.7   β”‚ 536.28    β”‚ 13591.96  β”‚ 4872.18  β”‚ 23,22,23,24,23,24,23,22,24,25,24,23,24,25,25 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Results for benchmark deeptree-useReduxAlternative2:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Version     β”‚ Avg FPS β”‚ Render       β”‚ Scripting β”‚ Rendering β”‚ Painting β”‚ FPS Values β”‚
β”‚             β”‚         β”‚ (Mount, Avg) β”‚           β”‚           β”‚          β”‚            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 7.0.0-hooks β”‚ 7.96    β”‚ 104.6, 4.6   β”‚ 15053.94  β”‚ 4996.25   β”‚ 1724.94  β”‚ 8,7,8,8    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Update: got some great review feedback from @MrWolfZ - I'm definitely on the drawing board still. But really happy to have this issue known to me now. Shall feedback with any updates as and when.


Hi all, joining this discussion as @MrWolfZ informed me that my library would suffer the same "zombie child" issue as discussed here.

I've managed to get around this by lifting the redux store subscriptions for the hooks to the root context, which allows me to execute the mapStates in an order that is more likely to represent the component hierarchy.

PR is here: ctrlplusb/easy-peasy#137

Demo: https://codesandbox.io/s/pm39mzn13m

I still need to do a lot more tinkering. But wanted to put this on your radar. Hoping to get some early feedback on whether this is a crazy idea or not. πŸ‘

Here is a very preliminary proof of concept branch with the HOC + hooks and here is a forked demo of the zombie child with the connect hooks

Don't take the code too seriously, I didn't spend much time on it (or test it). The only important thing here is the idea. I think we all knew that something like this would work but here is something a little more concrete.

Here's what it's doing (or at least trying to do):

  1. each wrap with connectHooks, creates a "subscription layer" where each call to useRedux will subscribe to a "hooksManager" and then the hooksManager will propagate to each useRedux hook in the "layer"
  2. All the updates to each useRedux subscriber in the layer is batched
  3. After the batched updates, the current layer will notify the next layers underneath it.

Checkout react-hooks-easy-redux by @dai-shi. It seems to me it dodges these issues entirely. The api in it is simply

const state = useReduxState();

No map state etc. It uses proxies to monitor what parts of the state is used in a given component and just forces rerender for the component if a monitored part of the state changes. So any state mapping you need to do you can do directly in the render. So in this case batchedUpdates should be enough to prevent issues with stale props?

At least it passes this test https://codesandbox.io/s/2p4kxnxj1j

Here's some more discussions at dai-shi/reactive-react-redux#10

Not 100% sure how performant this solution is.

Also obvious downside of it is the requirement for Proxies so IE11 support can be a challenge.

An even simpler API and it doesn't suffer from inconsistent props? What a gem.

Also obvious downside of it is the requirement for Proxies so IE11 support can be a challenge.

Unfortunately, I have to support IE 11 :( . Since we have the properties we want to proxy from the store, we can iterate through them and then use Object.defineProperty for the proxy mechanism instead of ES6 proxies.

Yeah, I think immer does it like that if there's no Proxy support https://github.com/mweststrate/immer#immer-on-older-javascript-environments

If I understand correctly another downside in react-hooks-easy-redux is that you cannot bail rendering if you only use derived state to produce output.

Ex.

function HasComments(props) {
	const state = useReduxState();
	const hasComments = state.posts[props.postId].comments.length > 0;
    return hasComments ? "yes" : "no";
}

This component would always render when comment count changes. Not just when it goes from 0 to something else.

@epeli not with the fork of redux i'm working on where selectors are created at store creation time: createStore(reducer, selectors, etc).

See:

@epeli I should note that react-hooks-easy-redux also has issues with Concurrent Mode, since it uses side effects in render (by attaching side effects to property accesses).

react-hooks-easy-redux also has issues with Concurrent Mode, since it uses side effects in render

It should work in Concurrnt Mode. It only updates refs in commit phase.

@dai-shi I really like the elegance of the proxy approach. Here are my two cents on the pros and cons:

pros:

  • the API is super easy to use and pretty much impossible to mis-use
  • your performance tests show it performs quite nicely
  • it only requires a single useReduxState call per component while with alternative approaches people may use multiple hook invocations which will in turn create multiple subscriptions etc.
  • no issue with stale props
  • (personal opinion) there is a certain beauty to marrying the redux and mobx approaches

cons:

  • it introduces additional dependencies on extra libraries (could be solved with inlining, but will still increase lib size)
  • requires a polyfill to support IE
  • it adds "magic" to the lib (although the success of mobx et al. means this seems to be not much of an issue; also there is already lots of "magic" involved in hooks anyways)
  • there are some cases in which unnecessary renders are triggered due to intermediary values being required to produce the final derived value (e.g. the example @epeli posted above, conditional accesses like from @faceyspacey's comment, or something simple like Object.keys(state.items))
  • it makes render impure since through the proxy it writes to a mutable reference (yes, it only assigns ref.current inside effects but e.g. trapped.reset() etc. during render mutate the objects inside those references); that said, while this means it is not strictly pure, all mutations your library currently performs are "predictable" and reproducible, so the render still has the pure property of same inputs = same outputs

One way to solve the intermediary value accesses without needing a redux fork or similar is to still use an API which passes the selector to the hook, but then uses proxies to trace property accesses inside the selector to determine equality. This would alleviate the stale props issue since something like items[id].prop will not throw an error if the item with id does not exist anymore.

Final note regarding concurrent mode: As @markerikson noted in a source code comment, I believe as well that any solution which accesses store.getState() during render will have issues with concurrent mode, which includes your library.

@MrWolfZ Thanks for your attention to the library and your detailed pros/cons!
I will refer your comment in my issues for further discussion.

@MrWolfZ

it makes render impure since through the proxy

That's not so much unlike the react-redux hoc which is impure as a parent component doing the work. There's gonna be impurity somewhere until React builds a global state store into React itself (and passes it down to all as a 2nd argument to all components), and it's very clear they have gone the opposite direction of Redux (probably because its lack of modularity).

One way to solve the intermediary value accesses without needing a redux fork or similar is to still use an API which passes the selector to the hook, but then uses proxies to trace property accesses inside the selector to determine equality

@epeli was saying that the closure is the real problem. So if he's correct and if the selectors weren't defined in the component function, or at least didn't enclose any variables, and rather the variables were passed separately, I think you're right, and selectors could work without a fork! Eg:

const hasComments = (state, postId) => {
   return state.posts[postId].comments.length > 0
}

const MyComponent = props => {
    const deps = [props.postId]
    const state = useReduxState(hasComments, deps)
    return <div>{hasComments ? 'yes' : 'no'</div>
}

That should work, as there is no difference between passing hasComments there or on store creation.

lot of good brainstorming happening around @dai-shi's library! (and not to mention @theKashey's lib: proxyequal which is the basis for all this!!

Lately, I'm designing an experimental selector based hook in my project https://github.com/dai-shi/reactive-react-redux (the project name changed.) I ended up with developing something similar to what @MrWolfZ proposed in #1179 (comment) with Proxy.

If Proxy-based approach is still immature for react-redux v7, and I suppose so, @MrWolfZ 's hook would be good for the official react-redux hook as it keeps the same mental model. Just my two cents.

Yeah, unfortunately, I don't think I can justify using anything that relies on Proxies completely atm. I ran a poll the other day, and it looks like 50% of React-Redux users are still targeting ES5 environments: https://twitter.com/acemarke/status/1111050066532265984 .

Not all proxy features are needed. And what is needed can be addressed under the hood (aka polyfilled), so that IE11 can work with no different work required of the end user. Proxy support isn’t in fact a barrier.

To be clear, not all of proxy features can be pollyfilled. But all that’s needed for this solution can be polyfilled.

https://github.com/GoogleChrome/proxy-polyfill - only ownKeys trap is not supported

@faceyspacey @theKashey there is one edge case where the polyfill doesn't work: if you have an optional property on the root state and add that property later on it will not notice this new property. Here is a reproduction. In this example, if you click "Add property", it will not cause Child to re-render.

However, this issue is very unlikely to surface in a real application, since it only happens for optional properties on the root state. For more nested properties it will simply be less performant, since it will stop the access detection chain at the parent of the optional property and then re-render Child whenever the parent changes. Here is an example showing this. In this example, the render count will not increase when clicking "Add property 2" without the polyfill but will increase with the polyfill.

(You can force the polyfill to be used even in modern browsers by just adding a <script>delete Proxy</script> to public/index.html's <head/>.)

@Jessidhia thank you for the hint. I tried deleting the global proxy object inside index.js but couldn't get it to work properly. With your suggestion it works perfectly, so I adjusted the sandboxes.

@MrWolfZ - the example is awesome - short and sound. And I have no idea why it's not working.


So - if you are accessing some prop, which does not exist - the access would not be recorded, thus the changes in that prop would be ignored.
That might be a big issue, actually, @ctrlplusb already created the same issue, but with another way to reproduce. I am afraid, but any change in object keys should be considered as a major update. And, huh, that would cause some performance implications.

@MrWolfZ : "optional properties on the root state" sounds exactly like a typical "dynamically loaded code-split reducers" scenario.

I don't see any technical way to solve this case without proxies, but there is a non-technical way.

  • in dev/test mode, where Proxies exists, detect access to such variables, and or ask a user to provide an initial state for a branch, you may replaceReducer later, or ask to use Object.keys and mark a selector as dependent o keys, and once the got changed - it would be recalculated.

Honestly - the first variant is more acceptable.

General Status Update

I've spent the last few hours doing research. I've re-read #1063 and this issue, put together a list of existing unofficial Redux hooks libs, looked at some of the code snippets pasted in these threads, and glanced through the source of reactive-react-redux and proxyequal.

I don't have any grand insights or solutions atm - I'm still trying to absorb a lot of info.

Notes and Thoughts

My immediate random thoughts, in no particular order:

  • The best of the existing unofficial hooks libs is easily https://github.com/facebookincubator/redux-react-hook , both in terms of actual popularity and how much thought has gone into it. Runners-up:

  • Pros and cons of the most promising approaches I've seen so far for hooks implementations that try to handle the "stale props / zombie child" issues:

  • Interestingly, the "Zombie Child" demo I'd created that fails in v7-alpha.5 actually appears to work okay with v7-beta.1. Not sure what specifically changed to make that happen.

  • There were mentions of needing to put some kind of a "intermediate HOC" in the tree to act as a sort of store update propagation layer. I think that connect() itself would serve that purpose, particularly with the v7 implementation. Right now, I'm pretty sure that connect() overrides the Subscription in context even if it doesn't subscribe to the store itself. (Which, come to think of it, might not even be the behavior we want?)

  • If we somehow had a way to track subscriptions in correct nested order, without relying on overriding a context value, I think that would resolve most of the issues we're discussing around stale props and stuff. That said, I truly don't know if that's even possible, given that we don't know where a given component is in the tree when it tries to subscribe.

  • Definitely punting on Concurrent Mode worries for now. Honestly, all things considered, I don't think the Redux core ever will be truly CM-compatible. That said, we still can't say much for sure until CM is actually out.

  • Following on from my earlier poll that showed 50% of React-Redux users still target ES5, I asked about whether it's okay for a new hooks API to be ES6+ only. 62% say yes, it is. I've also got a poll up asking who should be responsible for polyfilling, the library or the end user, and early results are 65% say the user.

Possible Path Forward

So here's a hypothetical suggested path towards actually building this:

  • Use something like the "alternative 1" approach, and ship these hooks:
    • useSelect(): subscribe-only
    • useActions(): action creators only
    • useDispatch(): just dispatch
    • useStore(): get the store if you really need it
    • useRedux(): roughly equivalent to connect() now
  • Consider shipping an additional useTrackedSelect() or something like that that would be based on the work from reactive-react-redux.
    • Might not be in the first hooks release, but a bit down the road.
    • It would be an opt-in thing - you can still use connect() and even useSelect() in an ES5-only if you want.
    • We could also consider shipping it and saying "If you need to target ES5, add the polyfill yourself"
    • Bundle size is a concern here. proxyequal is a few K by itself. I wouldn't want to force folks to pull that in unless they're using useTrackedSelect(). Would need to figure out how to get that set up well for tree-shaking.

Thoughts?

Yeah. I have one Thought, not related to this topic, but better be answered here.

In short - I have no idea how and why people are ok with redux. Unless you are an experienced developer with quite a special mindset, it's quite hard to create a proper boilerplate around selection from state and derived data creating in mapStateToProps.

To be clear - the problem is not with redux, the problem is with memoization it requires to live happily ever after without cascade updates.

Reselect is fragile, and not component-architecture compatible by design. re-reselect had never been an option for me. React.useMemo could be a great solution, but it has to live within React render cycle, which affects API.
I've tried my best - kashe as a solution for derived data generation/memoization and memoize-state (proxyequal) to remove than engeneering aspect from memoization.

So - my thoughts are simple - we need not a hook-based API, but an API there is would be or harder to make a mistake, or easier to write more sound code.

99% redux customers are doing it wrong, not knowing/checking what they are doing, or where/why memoization fails. Every time I convince someone to install why-did-you-update-redux and actually test "quality" of their "redux" - it's 🀯. Using custom equality check to perform deep comparison in reselect or areMergedPropsEqual - easy. Not using any memoization at all - even easier. Babel optional chaining (returning a new "empty" object every time) - a new trend.

A generic hook solution, like redux-react-hook is good, and looks quite similar to the "old" redux, but useMappedState like a forcing you to continue use hooks, especially useMemo, inside mapStateToProps, while you cant. It's +10 WTF per minute.

What if first_ try to create an addition to react-dev-tools(aka reselect-tools), to help you build/visualize selectors according to your factual state and factual usage (dev tools could track both), and then build hooks API in a way to easier utilize results of such tool.

PS: your concerns about proxyequal size are not quite correct. It's a low-level library which does not handle edge cases. Memoize-state, which build on top of it to handle those cases is twice bigger.

@theKashey amen, preach!

Basically, the only way the bell curve of developers (according to skill level) will ever be able to build properly rendering apps is through a proxy-based connectless future. Having to configure connect is also an absolute waste of developer time, given there is a path forward where we don't need it. connect will inevitably die, and the sooner the better.

I've been talkin about this for 2 years. It's time to level up the Redux world. I personally have chosen to focus on other things, FYI, because this is inevitable. It's like how Marc Andreesen talks about industries like the music industry (and now many others) inevitably being toppled: "technology is like water; it inevitably fills all the gaps and there's nothing we can do to stop it; we're better off supporting the natural direction of growth." That's not the exact quote, but you get the idea: we're best off facilitating a proxy-based connectless future.

So the most important thing the Redux ecosystem needs isn't a "hooks" API. HOCs still work beautifully. What needs to be determined is if hooks truly provide much value here (which they don't) or if we're just doing things because the React team opened up a new shiney API.

The thing is this:

  • the proxy-based approach looks better as hooks (if ur just using reducer state directly, no mapStatesToProps is needed)
  • the proxy-based approach seems to actually have less problems with stale props

If we want to "be like water" (this time in the Bruce Lee way), and hit a few birds with one stone, react-redux 8 should be hooks + proxies. 7 remains for everyone that can't support proxies yet.

Lastly, there are workarounds for supporting proxies. Namely what MobX always did where keys must be known upfront, as @theKashey mentioned. So in 8.5, perhaps that's introduced so even IE11 can make use of the transparent proxy-based approach. @theKashey has a few ideas how to handle this. But obsessing over it is not the path of the people that actually get this done. Build it first without supporting the non-proxy approach, and then circle back, like the Vue team is doing with Vue 3:

yes, out the gate, Vue 3 will not support IE11 and browsers that dont support Proxies.

In conclusion, the best use of available energies is toward helping @dai-shi and @theKashey optimize their proxy-based approach in their 2 libs:

  • react-react-redux
  • proxyequal - this library does most the heavy lifting FYI, and was made almost a year and a half ago by @theKashey. It needs some more work to get to where we need it though.

They are doing something truly groundbreaking. I usually don't poke my head out in these threads--so the whole purpose of my post here is to make it known how important their work is. The other hooks approaches are all following traditional approaches. Where's the revolutionary spirit? That's the whole point of software after all.

There's some remaining perf issues, but it's clearly achievable. What those 2 have done has proved a lot already. The perf for a great number of cases is comparable to react-redux 7. The one that's sub-par is a contrived case. It needs to be addressed regardless. The point is: we're close, and would be closer if more from the community got serious about this approach, rather than treating it like some far off thing. Of course current react-redux users are tied to IE11. But it's a totally different story for apps starting today. Not offering a futuristic approach, and not making that the main focus, will only mean Redux falls more and more out of favor. Bold action is required. Long live Redux!

@theKashey , @faceyspacey : fwiw, I definitely appreciate the work you and @dai-shi are doing investigating various Proxy-based approaches, especially given that I don't have time to try any of that myself.

That said, given React-Redux's "official" status, and the fact that it's one of the single most widely used libraries in the React ecosystem, we have to carefully balance a lot of different concerns: API design, browser compatibility, familiarity, teachability, community usages, versioning, and more. That means we do have to move a bit slowly and favor a conservative approach overall.

There's a very clear interest from the community in having us add some kind of hooks-based API, hence this issue. connect() is extremely well known, and there's a straight path from an API like mapState to useSelect() or similar. Adding something like useSelect() now doesn't preclude us from adding more APIs down the road.

But, anything we add now does have to continue to work and be supported going forwards. I refuse to put out new public APIs without ensuring they're tested and rock-solid, and I'm not going to force our users to keep bumping major versions (or even worse, break their code) because we did something bad. I feel bad enough about the v6 -> v7 change as it is - I'm the one who pushed for the v6 context-based state propagation. Granted, at the time it really did look like the way to go, but the churn here is basically my fault for choosing an approach that didn't pan out.

I agree that IE11 is (slowly) on the way out, and that use of Proxies is only going to increase over time. I think there's some great possibilities around using them, particularly for React-Redux. But, I'm not ready to jump that chasm yet.

I'd encourage you to continue your experiments and to work out the rough edges around these approaches. I've already been keeping an eye on what y'all have been doing, and I'm excited to see how it all turns out. When the time comes to try pulling some of those ideas into React-Redux itself, I will gladly learn from what you've done, give all of you full credit for any inspiration we take, and ask you to help contribute if possible.

Until then, the most straightforward path to getting something out the door is to build on the hooks-based code we've got in v7 right now, and provide hooks APIs that closely resemble use of connect().

Mark, I updated my post, make sure to check out what Vue 3 is doing where they wont initially support proxies:

https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf

I wouldn't be closed off to a react-redux going a similar route sooner than it seems you're currently considering. Vue after all is the whole framework, whereas react-redux is just one choice of many libs you can use with React.

There's 3 ways react-redux could get serious about it sooner:

  • support 2 major versions simultaneously (which isnt uncommon)
  • heavily promote an experimental branch
  • just start working on it internally without a pre-determined direction (i.e. add the branch and start writing the code; worry about how and what is released later; this will draw the whole react-redux community's attention to it)

Basically it would be nice if react-redux got serious about supporting this endeavor sooner than later. Right now @dai-shi and @theKashey need a lot more eyeballs than yours and mine.

Everyone else, HELP WANTED HERE:

https://github.com/dai-shi/reactive-react-redux

We need some established pattern for redux, and right now it does exist. Then we could try to create an eslint plugin, like react-team created for hooks(really - that's the deal breaker) and mitigate the majority "πŸ‘Ž"s.

So - let's focus not on the implementation details, but on the sound API, it would be possible to name as a pattern and create an infrastructure around.

My personal opinion - I love to separate concerns as much as possible (ie Smart/Dump) to make tests/storybooks easier. Right now all hooks realization are tightly coupled, they just form one big component, and does not answer this call.

So - Hooks vs Tests: the engage.

So the most important thing the Redux ecosystem needs isn't a "hooks" API. HOCs still work beautifully. What needs to be determined is if hooks truly provide much value here (which they don't) or if we're just doing things because the React team opened up a new shiney API.

@faceyspacey I don't necessarily agree with this. I care a lot about React hooks and a redux API because it simplifies composing together and reusing reactive dependency injectors (i.e. things that provide data + have the ability to case a re-render). Historically, I've been composing reactive dependency injectors via HOCs and that made the rest of my team scratch their heads e.g.

const Container = compose(
  withRouter,// get the `location` from react-router
  connect(/* ... */), // use the location + redux state to derive props
  lifecycle({
    componentDidUpdate() {
      // watch for changes in those props and refetch if necessary
    }
  }), 
)(PresentationalComponent);

export default Container;

with hooks, composing reactive dependency injectors looks much better/is more readable and useEffect is particular helpful

export default function Container() {
  const { location } = useRouter();
  const exampleState = useRedux(/* ... */);
  
  const calculatedProp = useMemo(() => {
    return // ...
  }, [location, exampleState]);

  useEffect(() => {
    // refetch data or similar
  }, [calculatedProp]);

  return <PresentationalComponent exampleProp={calculatedProp} />;
}

I think the above is a very "react" way of addressing the concerns @theKashey has with redux memoization. Don't get me wrong, @theKashey is 100% correct with the pains of memoization but hooks with useMemo might also be a good way to deal with it??

It's not fool proof but at least better than the current state of composing HOCs.


I like @markerikson proposed possible path forward simply because it gives me hooks and it's great incremental approach to improved patterns.

hooks with useMemo might also be a good way to deal with it??

That's a problem! They could not! 99% redux-hook implementation uses subscription mechanics to react to store changes and skip unnecessary updates, so your useMemo would be called by redux out of React rendering cycle. "WTF!", you can't use hooks in redux-hooks API, which is a bit controversial.