-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Discussion: Potential hooks API design #1179
Comments
Based on my experiments I've come up with following wishlist for the official redux hooks api: Provide low level primitives
Maybe these higher level APIs
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 This would also have the side-effect of creating a separate subscription per I had an idea to share subscriptions between // 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 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 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 function useStoreEffect(
effect: (state: StoreState) => void | (() => void | undefined),
// defaults to () => undefined
deps?: (state: StoreState) => ReadonlyArray<unknown> | undefined
): void It's like a |
Thinking about this as well and would suggest:
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)
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. |
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. |
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.
This feels overwrought, I suggested a simple wrapper around |
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:
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:
As "shouldn't impact performance". I'm concerned that hooks would be the ultimate foot-gun for:
But I'm not sure there's a good solution other than lots of evangelizing about the benefits of separation of concerns. |
I think this actually becomes less clear with hooks regardless. I think hooks makes it easier to understand and separate
export default function PresentationalComponent () {
return // ...
} connect HOC // connect container
import PresentationalComponent from 'blah/PresentationalComponent';
export default connect(
// etc...
)(PresentationalComponent); hooks Also addressing
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 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.
in your component, it's not straightforward whether this component is aware of Redux or not, unless you enforce conventions in your team, like:
|
@adamkleingit Not knowing that the component uses |
@markerikson on the related by different topic of releases would it make sense to work one (or all) of these proposals into a |
@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 I've tested it on my application and it works well as I see.
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;
}
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 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 |
@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 |
This may be a stupid question but would react-redux hooks depend on the new implementation of 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 Then again, it may also be different enough that there's nothing to reuse. Either way, though, you wouldn't use |
I actually have found the Ultimately, my Sure, one of the claims to hooks was to reduce the component hierarchy, and I think that it does still largely minimize it – when you use your I assume this will also aid in testing, as it's not really viable to use (Sorry for the aside, hope it's helpful to someone). |
As @markerikson suggested, I have created a PR with my hooks implementation. For the naming, I dropped the I have also dropped the |
Very cool to see this coming into shape. The new docs don't look quite right for the const todoSelector = useCallback(() => {
return state => state.todos[props.id]
}, [props.id])
const todo = useSelector(todoSelector) It seems like the const todoSelector = useCallback(
state => state.todos[props.id],
[props.id]
)
const todo = useSelector(todoSelector) The equivalent const todoSelector = useMemo(() => {
return state => state.todos[props.id]
}, [props.id])
const todo = useSelector(todoSelector) |
Sorry if I've missed the answer to this earlier, but what is the idea behind having the dependencies array in |
Would it be possible that My scenario is the following: in react native apps when you use react navigation for example, unlike on the web you don’t just render the screen that the user sees but also the whole stack of screens the user navigated over to get where he is and any parallel tabs. This is necessary for animations between screens etc.. Now the issue is you don’t want to rerender a bunch of hidden screens all the time but with redux all these components are subscribed to state updates and you can’t control when they update. In the past I’d just get a flag wether the component is corresponding to the currently active screen and then add a should component update HOC via recompose after the redux connect. But with hooks this doesn’t work anymore. I’d ideally like to be able to give a ref to the |
@MrLoh I don't think we should do this for such an edge case. However, my idea for this situation would be to just ensure the selector returns the same value as long as the active tab is not the tab the component is nested in. As long as you have the active tab in the state, this should be trivial to achieve. It should also be possible to turn this into a custom hook to select the state inside a tab. |
Closing this out since the code is merged in. |
@timdorr Well, it's only an alpha and I think there are still some design decisions to be made. |
@MrLoh you can conditionally pass a dud selector when a component is in the background. i.e const state = useSelector(inView ? actualSelector : ()=> null); That way when not in view the component would never re-render due to redux state changes. Although I can envision a few bugs potentially arising from an incorrect or stale |
@Dudeonyx I don't think this would work, as it would lead to a serenader as soon as the screen becomes inactive and everything goes to an empty/loading state. Nevertheless it should be possible to create a special memoized selector that saves and returns the last state when the screen is not inView |
Here's a crazy idea: Provide syntax sugar with exact same behaviour as P.S. I'm not experienced enough to tell if the latter is better served by context instead. |
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.
The text was updated successfully, but these errors were encountered: