Skip to content
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

Closed
timdorr opened this issue Oct 25, 2018 · 85 comments
Closed

Provide React Hooks #1063

timdorr opened this issue Oct 25, 2018 · 85 comments

Comments

@timdorr
Copy link
Member

timdorr commented Oct 25, 2018

Today's the big day for React Hooks!

Assuming the API gets released, we should provide some hooks. Something like useRedux would be cool!

import * as actions from './actions'

function Counter() {
  const [count, {increment, decrement}] = useRedux(state => state.count, actions);
  
  return (
    <>
      Count: {count}
      <button onClick={() => increment()}>+</button>
      <button onClick={() => decrement()}>-</button>
    </>
  );
}

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, using useContext under the hood.

@joshmanders
Copy link

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.

@timdorr
Copy link
Member Author

timdorr commented Oct 25, 2018

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()

https://reactjs.org/docs/hooks-faq.html#what-do-hooks-mean-for-popular-apis-like-redux-connect-and-react-router

@markerikson
Copy link
Contributor

markerikson commented Oct 25, 2018

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.

@sag1v
Copy link
Contributor

sag1v commented Oct 25, 2018

looks legit. Isn't it a replacement for connect?

@markerikson
Copy link
Contributor

@sag1v : potentially, but only within function components (as is the case for all hooks).

@sag1v
Copy link
Contributor

sag1v commented Oct 25, 2018

@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);

@markerikson
Copy link
Contributor

markerikson commented Oct 25, 2018

Yeah, probably. Give Tim a break - this is new to all of us :)

@sag1v
Copy link
Contributor

sag1v commented Oct 25, 2018

Aw sorry didn't mean to offend, I just thought i was missing something. 😔

@markerikson
Copy link
Contributor

No worries :) Just pointing out that it was simply a typo.

@timdorr
Copy link
Member Author

timdorr commented Oct 26, 2018

Fixed!

@Matsemann
Copy link

Would this be able to fix long-standing issues (/weaknesses) from the current wrapping-implementation, like those shown in #210 ?

@ctrlplusb
Copy link

ctrlplusb commented Oct 26, 2018

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

@timdorr
Copy link
Member Author

timdorr commented Oct 26, 2018

I could see useStore and useAction as piecemeal alternatives to the full-flavor useRedux hook.

@hnordt
Copy link

hnordt commented Oct 26, 2018

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)

@markerikson
Copy link
Contributor

markerikson commented Oct 26, 2018

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.

@markerikson
Copy link
Contributor

@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.

@hnordt
Copy link

hnordt commented Oct 26, 2018

@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.

@markerikson
Copy link
Contributor

markerikson commented Oct 26, 2018

@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.

@markerikson
Copy link
Contributor

markerikson commented Oct 26, 2018

@hnordt
Copy link

hnordt commented Oct 26, 2018

@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

@JesseChrestler
Copy link

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!

https://codesandbox.io/s/mm0qq8p43x

@JesseChrestler
Copy link

JesseChrestler commented Oct 27, 2018

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:
image

@timdorr
Copy link
Member Author

timdorr commented Oct 28, 2018

@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.

@JesseChrestler
Copy link

JesseChrestler commented Oct 28, 2018

@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.

@devthiago
Copy link

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>
  )
}

@mizchi
Copy link

mizchi commented Nov 1, 2018

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.

@edkalina
Copy link

edkalina commented Nov 3, 2018

@JesseChrestler alternative version for you string variant:

const items = useStoreValue`todos.items`;

@markerikson
Copy link
Contributor

@adamkleingit : yup, that's one of the reasons why I wrote that post :)

Answering your questions:

  1. I haven't tried checking what order useEffect calls run in. However, the "tearing" concern is about more than the top-down aspect - it's about different components possibly using different store state during the same render pass.
  2. Not sure what you're asking here.
  3. Also not sure what you're asking here either. As I said in my last comment, see Provide more ways to bail out inside Hooks facebook/react#14110 for our concerns with using new context, and function components re-rendering.
  4. Yeah, definitely not following your train of thought here. Why in the world would you debounce a selector? How would a useSelector hook make use of isEqual?

@adamkleingit
Copy link

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.
So the current direction is to keep the state in context, and wait for React to solve the problem of bailing out of re-render with useContext?

@markerikson
Copy link
Contributor

Basically, yeah.

@adamkleingit
Copy link

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:
https://codesandbox.io/s/y31zop4nxz

@adamkleingit
Copy link

adamkleingit commented Dec 21, 2018

BTW, doesn't React already handle this internally?
I mean, the update function returned as 2nd arg from useState synchronously updates the internal state saved in React, but asynchronously forces the components to re-render.
Am I right?

@markerikson
Copy link
Contributor

No, the state is not synchronously updated. The queued state changes are applied as part of the re-rendering process.

@adamkleingit
Copy link

But are they all applied before the rendering starts? Or they might occur alongside rendering?
BTW, what do you think about my proposed solution instead of using context?

@markerikson
Copy link
Contributor

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.

@littlepoolshark

This comment has been minimized.

@sampathsris
Copy link

IMO useReducer covers a lot of use cases. Yes, there is no central store in this approach. And bye-bye to serializable state and hot reloading?

@linde12
Copy link

linde12 commented Jan 8, 2019

IMO useReducer covers a lot of use cases. Yes, there is no central store in this approach. And bye-bye to serializable state and hot reloading?

To me, useReducer is just a cleaner way to manage my components internal state. Or for really small applications i might use it at the top level. I definitely don't see it as a redux replacement.

Whatever we end up doing i wish that useStoreState, useSelector, or w/e its named would be able to bind actions with bindActionCreators from redux. It's convenient to be able to have a actions object which you can pass as your callbacks. E.g. <button onClick={actions.fetchMore}>Fetch more</button> rather than having to destructure N actions or keep N actions in an array and refer to them by index.

@cain-wang
Copy link

cain-wang commented Jan 15, 2019

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))
  // ...
}

@YaaMe
Copy link

YaaMe commented Jan 15, 2019

How about contexts.js:

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);

@pgarciacamou

This comment has been minimized.

@jedrichards
Copy link

jedrichards commented Jan 29, 2019

@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/

@thomschke
Copy link

please see facebook/react#14110 (comment)

@esamattis
Copy link
Contributor

esamattis commented Jan 29, 2019

That will probably solve the performance issue but I wonder if putting partial states to local state using useState results in the tearing issues again (#86)?

@pgarciacamou
Copy link

Thanks @jedrichards, @thomschke, and @epeli! It all makes more sense now.

@markerikson
Copy link
Contributor

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.

@kvedantmahajan
Copy link

kvedantmahajan commented Jan 30, 2019

Please use redux-react-hook from facebook incubator. They do provide the equivalent of mapState and mapDispatch which are enough bindings to setup the workflow in React hooks. I've been working with Hooks version since last month

@iusehooks

This comment has been minimized.

@klis87

This comment has been minimized.

@MrLoh
Copy link

MrLoh commented Jan 30, 2019

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.

@kvedantmahajan

This comment has been minimized.

@markerikson
Copy link
Contributor

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.

@reduxjs reduxjs locked as too heated and limited conversation to collaborators Jan 31, 2019
@markerikson
Copy link
Contributor

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests