-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide React Hooks #1063
Comments
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 😄
|
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 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 |
@sag1v : potentially, but only within function components (as is the case for all hooks). |
@markerikson Yeah of course. Shouldn't it be: Or: |
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. 😔 |
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 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 |
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 When we call However, if I were to do something like 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 |
@markerikson I've updated my comment, did you see
|
@hnordt : I'm specifically talking about a |
A few other https://github.com/philipp-spiess/use-store |
@markerikson I think the "spirit" of hooks is based on small units of work. From Hooks docs:
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. |
@JesseChrestler I imagine we'll provide both the individual |
@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 |
@JesseChrestler alternative version for you string variant: const items = useStoreValue`todos.items`; |
@adamkleingit : yup, that's one of the reasons why I wrote that post :) Answering your questions:
|
I understand the problem now :) So for example parent component subscription gets called => parent state is updated => parent re-renders => child re-renders => child takes latest selector value which is outdated. |
Basically, yeah. |
What about, instead of passing the state through context, managing a list of selectors, and lazily subscribing to the store only once? Then if the state changes, go over the list of selectors and invoke them using the same state. And if the return value changed - call the useState callback (which is also saved alongside the selector)? Like this: |
BTW, doesn't React already handle this internally? |
No, the state is not synchronously updated. The queued state changes are applied as part of the re-rendering process. |
But are they all applied before the rendering starts? Or they might occur alongside rendering? |
State updates are, as far as I know, effectively applied during the rendering process. So, as React prepares to render a given component, it looks at the queued state updates , runs them, and finishes actually rendering. My guess is that any approach that still relies on direct subscriptions is likely to still have tearing issues down the road. |
This comment has been minimized.
This comment has been minimized.
IMO |
To me, Whatever we end up doing i wish that |
Quite excited about hooks support in react-redux!! Our team is experimenting with a useStore() API (with Flow support), useStore reads from a context. // store.js
// @flow
import { createStore, thunkMiddleware } from "@xxxx/redux";
export const { StoreProvider, useStore } = createStore<StoreState>(rootReducer);
// app.js
import { StoreProvider } from "./store";
export function App() {
return (
<StoreProvider>
<NumberInput />
</StoreProvider>
);
}
export function NumberInput() {
// useStore will automatically infer the store state flow type here!
const [{ numberState }, dispatch] = useStore();
const onChange = e =>
dispatch({ type: "UPDATE_NUMBER", value: e.target.value });
return (
<div>
Number:
<input value={numberState.number} onChange={onChange} />
</div>
);
} To support something similar to thunks, our lib ended up with a simple implementation of middlewares: // store.js
import { createStore, thunkMiddleware } from "@xxxx/redux";
export const { StoreProvider, useStore } = createStore<StoreState>(rootReducer,
[
thunkMiddleware,
/* ...middlewares */
]
);
// number-input.js
import {inputNumber} from './actions';
export function NumberInput() {
const [{ numberState }, dispatch] = useStore();
const onChange = e => dispatch(inputNumber(e.target.value))
// ...
} |
How about import React from 'react';
const formatConnectName = (
contextName
) => `connect${contextName.charAt(0).toUpperCase()}${contextName.slice(1)}`
const formatProviderName = (
contextName
) => `${contextName.charAt(0).toUpperCase()}${contextName.slice(1)}Provider`
const create = (contexts) => {
let Providers = {}
let connectors = {}
contexts.forEach(context => {
let {Provider, Consumer} = React.createContext(context);
Providers[formatProviderName(context)] = Provider;
connectors[formatConnectName(context)] = (
mapStateToProps, mapDispatchToProps
) => Target => props => (
<Consumer>
{store => (
<Target
{...mapStateToProps(store.getState())}
{...mapDispatchToProps(store.dispatch)}
{...props}
/>)}
</Consumer>
)
})
return {Providers, connectors}
};
const contexts = [
'app'
];
const Contexts = create(contexts);
export const Providers = Contexts.Providers;
export const connectors = Contexts.connectors; for usage import React, { Component } from 'react';
import Sample from 'containers/Sample';
import { Providers } from 'contexts';
import store from 'store';
const { AppProvider } = Providers;
class App extends Component {
render() {
return (
<AppProvider value={store}>
<Sample/>
</AppProvider>
);
}
}
export default App; and import React, { Component } from 'react';
import { connectors } from 'contexts';
const mapStateToProps = ({ test: { testid }}) => ({ testid })
const mapDispatchToProps = dispatch => ({
test: dispatch({ type: 'TEST' })
})
const Sample = ({ testid }) => (
<div className={sample}>
<div>sample: {testid}</div>
</div>
);
export default connectors.connectApp(mapStateToProps, mapDispatchToProps)(Sample); |
This comment has been minimized.
This comment has been minimized.
@pgarciacamou That looks like a nice DX, but as far as I understand it the reason this thread has stalled a bit is because there are some low level issues around React concurrent mode and Redux hook implementations that subscribe to the store itself via context vs. subscribing to the state of the store from the top of the component tree via context - with the former being undesirable. I think nearly all of the proposed implementations posted here so far haven't addressed this, and we're still waiting on some decisions upstream in React itself. @markerikson mentioned it in a comment here: #1063 (comment) More background in his blog post here: https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/ |
please see facebook/react#14110 (comment) |
That will probably solve the performance issue but I wonder if putting partial states to local state using |
Thanks @jedrichards, @thomschke, and @epeli! It all makes more sense now. |
As a quick FYI, we're doing some internal discussions to try to figure out a way forward regarding the hooks and perf aspects. I'll put up a roadmap issue once I've got an actual idea what we're going to do, but no ETA on that. |
Please use redux-react-hook from facebook incubator. They do provide the equivalent of |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Could you please refrain from spamming this thread with redux alternatives and asking the same questions all over again. Take the time to read the thread first as a courtesy to those who already have and are subscribed to it. This thread is about the hooks implementation for redux. Unless you have any insights about how to do the performance optimization needed to implement hooks for redux or are a maintainer that has updates, this is just alerting a ton of people continuously about nothing. |
This comment has been minimized.
This comment has been minimized.
Awright, this thread is starting to become useless. I'm going to lock it for the time being, but not close it. I'm unfortunately busy with day job stuff atm, so I haven't been able to turn my full attention to figuring out what we're going to do next. I hope to spend some time on that this weekend, but I can't guarantee anything. As I said, I'll post a roadmap issue and publicize it as soon as I actually have something meaningful to say. |
This issue has been superseded by the roadmap laid out in #1177 . Please see that issue for plans and any links to further discussions on this topic. |
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.The text was updated successfully, but these errors were encountered: