Closed timdorr closed 5 years ago
Great issue. I was curious how this hook would affect Redux. Good to see the team is already thinking about how it can be used WITH Redux. Subscribing.
BTW, this wasn't coordinated at all, but React is telling us to do this π
In the future, new versions of these libraries might also export custom Hooks such as
useRedux()
I'm already fiddling around with rewriting my #995 WIP PR for React-Redux v6 to use hooks internally instead of class components, and it looks way simpler so far. I hope to push up something for discussion within the next couple days.
As for actually exposing hooks... yeah, some kind of useRedux()
hook might be possible as well, but I haven't gotten my brain that far yet :)
edit
Huh. Actually, now that I look at Tim's example... yeah, that looks totally doable. I'd have to play around with things some more to figure out specific implementation, but I don't see any reason why we can't do that.
looks legit. Isn't it a replacement for connect
?
@sag1v : potentially, but only within function components (as is the case for all hooks).
@markerikson Yeah of course.
I'm a bit confused though, with this line:
const [state, {increment, decrement}] = useRedux(state => state.count, actions);
We are destructuring state.count
into a state
variable.
Shouldn't it be:
const [count, {increment, decrement}] = useRedux(state => state.count, actions);
Or:
const [state, {increment, decrement}] = useRedux(state => state., actions);
Yeah, probably. Give Tim a break - this is new to all of us :)
Aw sorry didn't mean to offend, I just thought i was missing something. :pensive:
No worries :) Just pointing out that it was simply a typo.
Fixed!
Would this be able to fix long-standing issues (/weaknesses) from the current wrapping-implementation, like those shown in #210 ?
Hey all, I experimented with a custom hook for a Redux store.
It's based on my library easy-peasy
which abstracts Redux but it returns a standard redux store, so this solution would work for Redux too. It's a naive implementation but just wanted to illustrate to everyone the possibilities.
import { useState, useEffect, useContext } from 'react'
import EasyPeasyContext from './easy-peasy-context'
export function useStore(mapState) {
const store = useContext(EasyPeasyContext)
const [state, setState] = useState(mapState(store.getState()))
useEffect(() =>
store.subscribe(() => {
const newState = mapState(store.getState())
if (state !== newState) {
setState(newState)
}
})
)
return state
}
export function useAction(mapActions) {
const store = useContext(EasyPeasyContext)
return mapActions(store.dispatch)
}
import React from 'react'
import { useStore, useAction } from './easy-peasy-hooks'
export default function Todos() {
const todos = useStore(state => state.todos.items)
const toggle = useAction(dispatch => dispatch.todos.toggle)
return (
<div>
<h1>Todos</h1>
{todos.map(todo => (
<div key={todo.id} onClick={() => toggle(todo.id)}>
{todo.text} {todo.done ? 'β
' : ''}
</div>
))}
</div>
)
}
See it in action here: https://codesandbox.io/s/woyn8xqk15
I could see useStore
and useAction
as piecemeal alternatives to the full-flavor useRedux
hook.
A naive implementation:
const useSelector = selector => {
const { getState } = useContext(ReduxContent)
const [result, setResult] = useState(selector(getState()))
useEffect(
() =>
store.subscribe(() => {
const nextResult = selector(getState())
if (shallowEqual(nextResult, result)) return
setResult(nextResult)
}),
[]
)
return result
}
const useActionCreator = actionCreator => {
const { dispatch } = useContext(ReduxContent)
return (...args) => dispatch(actionCreator(...args))
}
Usage:
const count = useSelector(state => state.count)
const increment = useActionCreator(increment)
I was thinking about this yesterday, after working on rewriting my #995 PR to use hooks internally.
There's an issue with how a hook like this would be written using our v6 approach. In v5, we put the store into legacy context, and the connect
components subscribe directly. In v6, we put the store state into createContext
, and the connect
components read the store state object from context.
When we call useContext(SomeContext)
, React marks that component as needing to re-render whenever the context updates, exactly the same as if we'd done <SomeContext.Consumer>{(value) => { }}</SomeContext.Consumer>
. That's fine with connect
, because we want the wrapper component to run its update process, check to see if the extracted values from mapState
have changed, and only re-render the wrapped child if those are different.
However, if I were to do something like const updatedData = useRedux(mapState, mapDispatch)
, then our function component would re-render if any part of the Redux state had changed, and there's currently no way to look at updatedData
and bail out of rendering this function component if it's the same as last time. @sophiebits and @gaearon confirmed the issue here: https://twitter.com/acemarke/status/1055694323847651335 .
Dan has filed React #14110: Provide more ways to bail out inside hooks to cover this. So, the issue is on their radar, and they'd like to have a way for function components to bail out of re-rendering before 16.7 goes final.
@Matsemann : using hooks won't fix the "dispatch in lifecycle methods" issue by itself, exactly. The switch to using createContext
in v6 is what would really matter.
@markerikson I've updated my comment, did you see useSelector
?
store.subscribe
will fire on every received action, but it'll bail out if that state slice didn't change.
@hnordt : I'm specifically talking about a useRedux()
hook that would be based on the v6 PRs, where we put the state into context rather than the whole store.
A few other useRedux()
-type implementations I've already seen:
https://github.com/philipp-spiess/use-store https://github.com/ianobermiller/redux-react-hook https://github.com/martynaskadisa/react-use-redux https://codesandbox.io/s/232nrwr19p (more of a "integrate with Redux DevTools" hook) https://github.com/brn/rrh
@markerikson I think the "spirit" of hooks is based on small units of work. useRedux
would be "too much" in my opinion.
From Hooks docs:
Separating independent state variables also has another benefit. It makes it easy to later extract some related logic into a custom Hook.
https://reactjs.org/docs/hooks-faq.html#should-i-use-one-or-many-state-variables
Here an example of what I would like to see for react-redux future implementation. Feel free to play with it or ask questions. Love the feedback!
Thinking more about this and was asking myself why can't we split apart the state, and dispatch? It would reduce what each hook is trying to do conceptually and be smaller re-usable parts. I organized my previous example and cleaned it up a bit more on a separate fork. Feel free to play around with it https://codesandbox.io/s/1o79n7o46q.
Simplest Example:
@JesseChrestler I imagine we'll provide both the individual use*
functions for state and actions, but also an all-in-one for those that want something that looks like connect() today.
@timdorr what about the different ways of retrieving state? I provided 3 ways to do it. I think the string variant is good for simplifying the example for new users. Having a single function is good for those already used to the way connect works and can easily port existing code. The object structure is more for when you've have the connect where you already have predefined selectors.
Single Function Example
const state = useReduxState(state => ({
count: countSelector(state),
user: userSelector(state)
})
Object Example
const state = useReduxState({
count: countSelector,
user: userSelector
})
I think for larger projects having this object notation cleans up a lot of noise around mapping data. I suppose this can be pushed off on the user to implement and they would map their object with this single function. It could look like this.
Sample implementation
const reduxObject = (selectorObject) => (state) => Object.keys(selectorObject).reduce((selected, key) => {
selected[key] = selectorObject[key](state)
return selected;
}, {})
Sample use case
const state = useReduxState(reduxObject({
count: countSelector,
user: userSelector
}))
what do you think? I prefer to have this logic in the useReduxState, but wondering your thoughts on this.
Why not something like this:
import { useState, useEffect } from 'react'
import store from 'redux/store'
import objectCompare from 'libs/objectCompare'
const emptyFunction = () => ({})
export default function useRedux(mapStateToProps = emptyFunction, mapDispatchToProps = emptyFunction) {
const stateToProps = mapStateToProps(store.getState())
const dispatchToProps = mapDispatchToProps(store.dispatch)
const [state, setState] = useState(stateToProps)
useEffect(() => store.subscribe(() => {
console.log(`Running subscribe`)
const newStateToProps = mapStateToProps(store.getState())
console.log('newStateToProps', newStateToProps)
console.log('stateToProps', stateToProps)
if (!objectCompare(newStateToProps, stateToProps)) {
console.log('setState')
setState(newStateToProps)
}
}))
return {
...state,
...dispatchToProps
}
}
import React from 'react'
import { useRedux } from 'hooks'
import { increaseCounter, nothingCounter } from 'redux/ducks/counter'
const mapStateToProps = ({ counter }) => ({ counter: counter.value })
const mapDispatchToProps = dispatch => ({
increase: () => dispatch(increaseCounter()),
nothing: () => dispatch(nothingCounter())
})
export default function Block1() {
const {
counter,
increase,
nothing
} = useRedux(mapStateToProps, mapDispatchToProps)
return (
<section>
<p>{counter}</p>
<button onClick={increase} children={'Click me'}/>
<button onClick={nothing} children={'Nothing'}/>
</section>
)
}
I tried to implement type safe version with typescript.
// full implementation https://gist.github.com/mizchi/5ab148dd5c3ad6dea3b6c765540f6b73
type RootState = {...};
const store = createStore(...);
// Create React.Context and useXXX helpers with State
const { Provider, useStore, useSelector } = createStoreContext<RootState>();
// State user
function CounterValue() {
// Update only !isEqual(prevMapped, nextMapped)
const counter = useSelector(state => ({ value: state.counter.value }));
return <span>counter.value: {counter.value}</span>;
}
// Dispatch user
function CounterController() {
const { dispatch } = useStore(); // or just return dispatch to be safe?
const onClickPlus = useCallback(() => {
dispatch({ type: INCREMENT });
}, []);
const onClickIncrementHidden = useCallback(() => {
dispatch({ type: INCREMENT_HIDDEN_VALUE }); // will be skipped by CounterView
}, []);
return (
<>
<button onClick={onClickPlus}>+</button>
<button onClick={onClickIncrementHidden}>+hidden</button>
</>
);
}
function CounterApp() {
return (
<div>
<CounterValue />
<hr />
<CounterController />
</div>
);
}
ReactDOM.render(
<Provider store={store}>
<CounterApp />
</Provider>,
document.querySelector(".root")
);
I do not have confidence useSelector
is correct name. (useMappedState(fn)
?)
IMO, name of redux (or Redux)
is just library name, not behavior.
@JesseChrestler alternative version for you string variant:
const items = useStoreValue`todos.items`;
@gokalina yes! though you'd have to split on the periods and iterate the object or have the key that is
{
'todos.items': []
}
But yeah even cleaner! π I'd really like to simplify the API as much as possible. For fun experiment I wanted how much i could reduce the code using useReducer/useContext to create Redux. It actually turned out to save code because it's already using the same life cycle events react uses. So the need to subscribe vanished. I also no longer need to store state into my component for it to re-render. that's already handled by the context. Much cleaner IMHO.
using Redux:
import {useContext, useState, useEffect} from 'react'
import ReduxContext from './ReduxContext'
import {getReduxValue, createCompare} from './helper'
const useReduxState = mapState => {
const store = useContext(ReduxContext)
let initialState = getReduxValue(store, mapState)
const [state, setState] = useState(initialState)
const compareState = createCompare(nextState => setState(nextState))
useEffect(
() => {
return store.subscribe(() => {
compareState(getReduxValue(store, mapState))
})
},
[mapState],
)
return state
}
export default useReduxState
using useReducer/useContext:
import {useContext} from 'react'
import ReduxContext from './ReduxContext'
import {getReduxValue} from './helper'
const useReduxState = mapState => {
const [state] = useContext(ReduxContext)
return getReduxValue(state, mapState)
}
export default useReduxState
If you're interested you can see my codesandbox here https://codesandbox.io/s/7ywwo0m690
Don't want to be the Debbie Downer at this party, but should we not maybe aim to
get a release out that supports the current, existing, and stable broken things
(like getDerivedStateFromProps
) before re-writing/exposing an early-stage
proposal?
@deecewan : we're working on it :) See #1000. I was hoping to get through my cleanup work on that this weekend, but was otherwise occupied. Hopefully in the next couple days.
Also, note that most of the discussion in this thread has been from others besides Tim and myself.
Nice! Been following along for ages, so very keen to see it coming along.
Noted. I guess people should be able to work on what they want to work on, regardless. But I'd imagine that most people can't even use hooks yet, given they only exist on an alpha branch.
Also, noted that Tim did mention that this is a future issue for when hooks land.
Yep, this is just a tracking issue to let others know we know and to make sure we implement them when/if they're available.
@ctrlplusb
I'm just thinking about the design of these hooks a bit - I think finer-grained hooks for actions and state make more sense than a coarse-grained useRedux hook. Tweaking your example slightly, what if it were like this:
import React from 'react'
import { useStoreState, useStoreAction } from 'react-redux'
import { toggle, clear } from './actions'
export default function Todos() {
// state mapping per value of interest
const todos = useStoreState(state => state.todos.items)
// useAction automatically binds action creators to dispatch
const onToggle = useStoreAction(toggle)
const onClear = useStoreAction(clear)
return (
<div>
<h1>Todos</h1>
<button onClick={onClear}>Clear Todos</button>
{todos.map(todo => (
<div key={todo.id} onClick={() => toggle(todo.id)}>
{todo.text} {todo.done ? 'β
' : ''}
</div>
))}
</div>
)
}
@darthtrevino : WhyNotBoth.jpg
:)
More seriously, I can imagine that we might offer all of those - useReduxState()
(mapState
by itself), useReduxActions()
(mapDispatch
by itself), and a combined useRedux()
that does both (and probably uses the other two internally or something).
Most of this logic is already broken out internally in the memoized selectors that make up connect()
's logic right now anyway, we'd just need to reshuffle the pieces.
However, we're going to need React to support bailing out of context-triggered re-renders first. Dan just filed https://github.com/facebook/react/issues/14110 to cover that.
That's fair, I just think it's worth working through the design alternatives a bit before committing to any
I'm just stoked AF to use hooks everywhere
react useReducer
const [state, dispatch] = useReducer(reducer, initialState);
As @timdorr stated in the first post
Yes, I'm aware of useReducer. That is for a Redux-style reducer, but for a local component's state only.
@markerikson you mentionned links:
https://github.com/philipp-spiess/use-store
const [substate, dispatch] = useSubstate(state => { return { count: state.count }; });
possible clash of dispatch concept with useReducer
https://github.com/ianobermiller/redux-react-hook
const dispatch = useDispatch();
possible clash of dispatch concept with useReducer
https://github.com/martynaskadisa/react-use-redux this one is fine
and @timdorr proposal
const [count, {increment, decrement}] = useRedux(state => state.count, actions);
this one is fine too
I know i am taking it far i do understand the one from react is for local component state and the other one for redux state but if you can provide a solution that use actions name directly to trigger them without the need of dispatch
=> it would be great i think (@timdorr suggestion is cool)
Here's an idea for something that can be used to bail out on invoking the component, but this will only work with functions and not exotic components:
function withRedux(mapStateToProps, mapDispatchToProps) {
return function factory(componentFunction) {
Component.displayName =
componentFunction.displayName || componentFunction.name
return React.memo(Component)
function Component(props) {
const store = useContext(ReduxContext)
useEffect(
() => {
/* setup subscription */
},
[store]
)
const storeState = store.getState()
const mappedState = useMemo(() => mapStateToProps(storeState, props), [
storeState,
props
])
const mappedDispatch = useMemo(
() => mapDispatchToProps(store.dispatch.bind(store), props),
[store, props]
)
return useMemo(
() =>
componentFunction({
...props,
...mappedState,
...mappedDispatch
}),
[props, mappedState, mappedDispatch]
)
}
}
}
It also won't bail out on reconciling children, only on rendering this specific component.
EDIT: turns out it will; what timing π
More EDIT: seems this is basically what #1065 is doing anyway
PS: I wonder if this would be consistent with the rules of hooks:
const mappedState = mapStateToProps.length === 1
? useMemo(() => mapStateToProps(storeState), [storeState])
: useMemo(() => mapStateToProps(storeState, props), [storeState, props])
There is a conditional, but both sides of the conditional only invoke exactly one hook.
This also doesn't deal with the shallow equality checking of mapStateToProps and mapDispatchToProps themselves, which would probably have to go through another level of indirection.
All in all, this still has the same problems as the current connect
does, just with less layers in devtools. Hopefully we get a solution from the team soon (maybe something like throw React.noop
but that sounds way too powerful).
I might be arriving late to the party but this is how Iβve been doing global state with hooks.
Itβs far from a full solution but I wonder if it could be applied for Redux
https://codesandbox.io/embed/n31n1lw6ml
(Grabbing a local components state update function and adding/removing it as a subscription via useEffect)
BTW, to be clear about versioning stuff, here's what I'd like to do:
connect()
API. Require React >= 16.7. So, a minor and then a major. We go all-in, but gradually. Obviously, this depends a lot on how Hooks turn out, both from an API/pattern perspective and a performance/ergonomics perspective.
I'm surprised at how similar my implementation of this is to you guys'. I just stumbled upon this thread when i was going to check if react-redux was doing something with hooks π
Basically i've got something like:
const [count, actions, dispatch] = useStore('count', { increment, decrement })
You can choose between passing a string like 'a.b.c.d' vs a function like state => state.a.b.c.d
. Second argument to useStore
is an object of action creators that will be bound via bindActionCreators
the first render. useStore
returns an array containing: state, bound actions & the dispatch function (just in case :-)).
I've got two examples here, one very simple example (counter/Counter.js) and one using a reselect selector (users/UserList.js)
The implementation ignores changes to the arguments passed to useStore
after initial render, similar to what useState
does. This is so that we can memoize the bound action creators and the "expensive" string.split('.') (for now i use get
from lodash) computation every render.
See the sandbox: https://codesandbox.io/s/lx6yp1578z
Hello All,
I'm just coming from Angular to React (with typescript) and hoping to help keep the types a bit cleaner(and more extendable) than I've see in some of the more prominent libraries. I've distilled what seems to be the essence of these useRedux examples to the following: https://gist.github.com/baetheus/ee94b4cb172eefeafd1ab8c13abcf77e
The intent with mapDispatchToProps is specifically to hide dispatch from the component, and have it call functions with known names instead. This is helpful when testing the inner component as you don't have to provide a mock dispatch which needs to be aware of the action creator return type implementation details; just mock action creators that only have to care about what the input arguments are.
@Kovensky This makes sense for a higher order component passing functions to a presentational component, but does it makes sense in the context of a stateful component?
In the example I provided it would be the same amount of code to mapDispatchProps outside of the useRedux hook as it would inside. The difference is that by keeping the hook focused on selecting from the store with the dispatch mappings separate there is less for the useRedux hook to do. The coupling is looser, the cohesion is higher.
This is what I am using right now in an app written in TypeScript.
import { useContext } from 'react';
import { Action, Dispatch } from 'redux';
import { ReactReduxContext } from 'react-redux';
import { AppState } from 'modules/types';
export function useRedux<T extends Action>(): [AppState, Dispatch<T>] {
const { storeState: state, store: {dispatch}} = useContext(ReactReduxContext);
return [state, dispatch];
}
export default useRedux;
Hi,
I thinks we can handle with to hooks useDispatch and useStateMapper.
We can pass action to useDispatch and then it will return memoize function.
useStateMapper will return mapped key with redux store and return value, in this function we have to create useEffect hook as explained by @ctrlplusb but with shallowEqual
comparison.
useStateMapper should accept string or function to map.
Please See this code on code sand box to find out how these too hooks works exactly.
I have made a package named react-redux-peach will provide these two hooks and you can see and talk to me about this solution.
In react-redux-peach
package I have used another package named redux-peach, it combine action and its reducers in one object and make better DX to use redux and actions.
Typescript combining a lot of ideas here + concat hook convenience and binding actions to dispatch:
const Context = createContext<Store<any>>(undefined as any);
const useSelector = <S, R, C>(selector: OutputSelector<S, R, C>) => {
const store = useContext(Context);
const [ state, setState ] = useState(selector(store.getState()));
useEffect(() => store.subscribe(() => {
const newState = selector(store.getState());
if (state !== newState) setState(newState);
}));
return state;
};
const useDispatch = <A extends Index<any>>(actionsMap: A) => {
const store = useContext(Context);
return useMemo(() => Object
.keys(actionsMap)
.reduce<A>((acc, key) => {
acc[ key ] = (...args: any[]) => store.dispatch(actionsMap[ key ](...args));
return acc;
}, {} as any), [ actionsMap ]);
};
interface ConcatHooks {
<T1>(f1: () => T1): T1;
<T1, T2>(f1: () => T2, f2: () => T2): T1 & T2;
<T1, T2, T3>(f1: () => T1, f2: () => T2, f3: () => T3): T1 & T2 & T3;
}
const concatHooks: ConcatHooks = R.pipe(R.unapply(R.identity), R.chain(R.call), R.mergeAll);
const listings = createSelector(...);
const listing = createSelector(...);
const useListings = () => useSelector(listings);
const useListing = () => useSelector(listing);
const useActions = () => useDispatch({l listingSelected });
const Listings: React.SFC = memo(() => {
const { listings, listing, listingSelected } = concatHooks(useListings, useListing, useActions);
return (
<FlatList<any>
data={ listings }
renderItem={ (props) => (
<ListingItem
{ ...props }
onPress={ (item) => listingSelected({ key: item.key }) }
/>
) }
ListHeaderComponent={ () => (
<Text>{ listing ? listing.key : 'None selected' }</Text>
) }
/>
);
});
I created redux-hooker for those who are interested. Have a look at http://github.com/linde12/redux-hooker
Basically useStoreState is a subscribe with shallow eq check, useActions is a memoed bindActionCreators. I dont think it has to be much more complicated than that.
@linde12: nice! How could this be combined with memoized selectors? Thanks a lot for all your Initiatives, folks!
@PatricSachse do you mean like reselect?
In that case you would do it something like this:
const selectUserList = createSelector(...)
const userList = useStoreState(selectUserList)
Today's the big day for React Hooks!
Assuming the API gets released, we should provide some hooks. Something like
useRedux
would be cool!Note: Yes, I'm aware of
useReducer
. That is for a Redux-style reducer, but for a local component's state only. What we're building would be for the store's global state, usinguseContext
under the hood.