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

Open
timdorr opened this Issue Oct 25, 2018 · 58 comments

Comments

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

This comment has been minimized.

joshmanders commented Oct 25, 2018

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

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

sag1v commented Oct 25, 2018

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

@markerikson

This comment has been minimized.

Contributor

markerikson commented Oct 25, 2018

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

@sag1v

This comment has been minimized.

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

This comment has been minimized.

Contributor

markerikson commented Oct 25, 2018

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

@sag1v

This comment has been minimized.

sag1v commented Oct 25, 2018

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

@markerikson

This comment has been minimized.

Contributor

markerikson commented Oct 26, 2018

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

@timdorr

This comment has been minimized.

Member

timdorr commented Oct 26, 2018

Fixed!

@Matsemann

This comment has been minimized.

Matsemann commented Oct 26, 2018

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

@ctrlplusb

This comment has been minimized.

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

This comment has been minimized.

Member

timdorr commented Oct 26, 2018

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

@hnordt

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

markerikson commented Oct 26, 2018

@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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

markerikson commented Oct 26, 2018

@hnordt

This comment has been minimized.

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

This comment has been minimized.

JesseChrestler commented Oct 26, 2018

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

devthiago commented Oct 31, 2018

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

This comment has been minimized.

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.

@gokalina

This comment has been minimized.

gokalina commented Nov 3, 2018

@JesseChrestler alternative version for you string variant:

const items = useStoreValue`todos.items`;
@darthtrevino

This comment has been minimized.

darthtrevino commented Nov 5, 2018

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

@darthtrevino

This comment has been minimized.

darthtrevino commented Nov 5, 2018

I'm just stoked AF to use hooks everywhere

@nasreddineskandrani

This comment has been minimized.

nasreddineskandrani commented Nov 5, 2018

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)

@Kovensky

This comment has been minimized.

Kovensky commented Nov 6, 2018

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

This comment has been minimized.

simmo commented Nov 7, 2018

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

This comment has been minimized.

Member

timdorr commented Nov 7, 2018

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

  • 6.x.0 - Add Hooks as a new API and maintain a React >= 16.4 compat. Add invariant checks around Hooks for React <16.7. No other changes to the library.
  • 7.0.0 - Start using Hooks internally for our existing 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.

@linde12

This comment has been minimized.

linde12 commented Nov 15, 2018

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

This comment has been minimized.

baetheus commented Nov 18, 2018

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

  • useReduxFactory takes a context as an argument. It assumes that the entire store is passed to it for now, but this could be extended to accept a frozen store reference (to avoid unnecessary rerenders) as was referenced in #1063 (comment)
  • useRedux accepts a selector and a comparator function. This keeps users from being stuck with insufficient diffing mechanisms between renders.
  • useRedux returns the selected state and the dispatch method. I'm really not sure why action factories are being passed through the useRedux hook. It doesn't seem like they are changed at all and would already be available to the scope of the function anyway.
  • Typescript types that inherit well from the existing redux and react typings.
@Kovensky

This comment has been minimized.

Kovensky commented Nov 18, 2018

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

This comment has been minimized.

baetheus commented Nov 18, 2018

@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

This comment has been minimized.

TroySchmidt commented Nov 18, 2018

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

This comment has been minimized.

borvelt commented Nov 25, 2018

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

This comment has been minimized.

chrismcleod commented Dec 5, 2018

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

This comment has been minimized.

linde12 commented Dec 8, 2018

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

This comment has been minimized.

PatricSachse commented Dec 8, 2018

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

@linde12

This comment has been minimized.

linde12 commented Dec 8, 2018

@PatricSachse do you mean like reselect?

In that case you would do it something like this:

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

This comment has been minimized.

Satyam commented Dec 8, 2018

A variation on some of the above: https://github.com/Satyam/lacorazon/blob/master/src/store/hooks.js

It has both useDispatch and useSelector and both allow to get a whole bunch of dispatch-bound action creators and selected values at once, for example (from https://github.com/Satyam/lacorazon/blob/master/src/User.js#L17-L25):

export default function User({ match }) {
  const id = match.params.id;
  const { history } = useReactRouter();
  const [user, isLoading] = useSelector([selUser, selUsersIsLoading], id);
  const [doGetUser, doSetUser, doDeleteUser] = useDispatch([
    getUser,
    setUser,
    deleteUser
  ]);
// ...

Only one subscription to the store and one useEffect is used per call to useSelector which, I guess, could save some resources on large applications over calling it repeatedly for each selector. You can also do like below, but it takes two separate subscriptions via useEffect:

  const user = useSelector(selUser, id);
  const isLoading = useSelector(selUsersIsLoading);

The selectors can be either functions or in the form of strings: (from https://github.com/Satyam/lacorazon/blob/master/src/store/users/selectors.js)

import { NAME } from './constants';
export const selUsers = NAME + '.data';
export const selUsersIsLoading = NAME + '.isLoading';
export const selUsersGotAll = NAME + '.gotAll';
export const selUser = NAME + '.data.%0';

The selUser selector could be written like this:

export const selUser = (state, id) => state[NAME].data[id];

The NAME constant tells where this particular slice of the store is kept, and also serves as a prefix for all action types, but this is not related to the hook in question (see: https://github.com/Satyam/lacorazon/blob/master/src/store/users/constants.js)

The whole app in that repository is just an exercise to test some ideas, mostly trying out hooks, not for production.

@jvitela

This comment has been minimized.

jvitela commented Dec 9, 2018

How about https://github.com/facebookincubator/redux-react-hook. Will this be the official Hooks for Redux?

@markerikson

This comment has been minimized.

Contributor

markerikson commented Dec 9, 2018

@jvitela : no. Any official hooks will be part of this package, React-Redux.

@adamkleingit

This comment has been minimized.

adamkleingit commented Dec 12, 2018

This is how I would write it (I tested it, it works):

Usage:

import { useActions, useSelector } from 'react-redux';

const MyComp = () => {
  const [setLocale] = useActions([actions.setLocale]); // Wraps actions with dispatch
  const locale = useSelector(state => state.locale.current); // Selects value and re-renders if it changes

  return (...);
}

Implementation:

import React, { useState, useContext, useEffect } from "react";

export const useStore = () => {
  return useContext(ReactReduxContext);
};

export const useActions = (...actions) => {
  const store = useStore();

  return actions.map(action => (...args) => store.dispatch(action(...args)));
};

export const useSelector = (selectorFn) => {
  const store = useStore();
  let [curResult, setCurResult] = useState(() =>
    selectorFn(store.getState())
  );

  useEffect(() => {
    return store.subscribe(() => {
      const nextResult = selectorFn(store.getState());

      if (nextResult !== curResult) {
        curResult = nextResult; // for this function closure
        setCurResult(nextResult); // for the component
      }
    });
  }, []);

  return curResult;
};
@adamkleingit

This comment has been minimized.

adamkleingit commented Dec 12, 2018

@hnordt your implementation will call the selector on each render. Check out my implementation above which uses an initializer function for useState to avoid this

@jedrichards

This comment has been minimized.

jedrichards commented Dec 12, 2018

I just wanna say 👍 to the memoized useSelector pattern emerging above, I think selectors as a pattern and naming convention should be promoted over and above arbitrary state mappers. useSelector and useAction feel like the obvious react-redux "hook primitives" to me, and then a way to extend those into pluralised versions useSelectors and useActions as a convenience.

@vincentjames501

This comment has been minimized.

vincentjames501 commented Dec 13, 2018

I like @adamkleingit 's approach to this. Simple and performant.

@markerikson

This comment has been minimized.

Contributor

markerikson commented Dec 14, 2018

I'll point out that these implementations that subscribe to the store directly are likely to have issues when used with React's concurrent mode. That's one of the big reasons why we switched from direct subscriptions to passing the current state value in context.

Please see my post Idiomatic Redux: The History and Implementation of React-Redux for background on this.

However, as mentioned earlier, computing a derived value from context does not allow bailing out if that value hasn't changed, so we can't currently build a hook that works this way. See the discussion in facebook/react#14110 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment