reduxjs / react-redux

Official React bindings for Redux
https://react-redux.js.org
MIT License
23.37k stars 3.37k forks source link

Provide React Hooks #1063

Closed timdorr closed 5 years ago

timdorr commented 5 years ago

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

timdorr commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

markerikson commented 5 years ago

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

sag1v commented 5 years ago

@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 commented 5 years ago

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

sag1v commented 5 years ago

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

markerikson commented 5 years ago

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

timdorr commented 5 years ago

Fixed!

Matsemann commented 5 years ago

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

ctrlplusb commented 5 years ago

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 commented 5 years ago

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

hnordt commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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

hnordt commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@JesseChrestler alternative version for you string variant:

const items = useStoreValue`todos.items`;
JesseChrestler commented 5 years ago

@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

deecewan commented 5 years ago

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?

markerikson commented 5 years ago

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

deecewan commented 5 years ago

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.

timdorr commented 5 years ago

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.

darthtrevino commented 5 years ago

@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>
  )
}
markerikson commented 5 years ago

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

darthtrevino commented 5 years ago

That's fair, I just think it's worth working through the design alternatives a bit before committing to any

darthtrevino commented 5 years ago

I'm just stoked AF to use hooks everywhere

nasreddineskandrani commented 5 years ago

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:

  1. https://github.com/philipp-spiess/use-store const [substate, dispatch] = useSubstate(state => { return { count: state.count }; }); possible clash of dispatch concept with useReducer

  2. https://github.com/ianobermiller/redux-react-hook const dispatch = useDispatch(); possible clash of dispatch concept with useReducer

  3. 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)

Jessidhia commented 5 years ago

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

simmo commented 5 years ago

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)

timdorr commented 5 years ago

BTW, to be clear about versioning stuff, here's what I'd like to do:

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.

linde12 commented 5 years ago

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

baetheus commented 5 years ago

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

Notable Differences

Jessidhia commented 5 years ago

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.

baetheus commented 5 years ago

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

TroySchmidt commented 5 years ago

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;
borvelt commented 5 years ago

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.

chrismcleod commented 5 years ago

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>
      ) }
    />
  );
});
linde12 commented 5 years ago

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.

PatricSachse commented 5 years ago

@linde12: nice! How could this be combined with memoized selectors? Thanks a lot for all your Initiatives, folks!

linde12 commented 5 years ago

@PatricSachse do you mean like reselect?

In that case you would do it something like this:

const selectUserList = createSelector(...)
const userList = useStoreState(selectUserList)