Git Product home page Git Product logo

redux-react-hook's Introduction

redux-react-hook

React hook for accessing mapped state and dispatch from a Redux store.

Build Status NPM Bundle Size

This project has been DEPRECATED

With the release of the hooks API in react-redux 7, this project has become redundant.

Table of Contents

Install

# Yarn
yarn add redux-react-hook

# NPM
npm install --save redux-react-hook

Quick Start

//
// Bootstrap your app
//
import {StoreContext} from 'redux-react-hook';

ReactDOM.render(
  <StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>,
  document.getElementById('root'),
);
//
// Individual components
//
import {useDispatch, useMappedState} from 'redux-react-hook';
import shallowEqual from 'shallowequal';

export function DeleteButton({index}) {
  // Declare your memoized mapState function
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // Get data from and subscribe to the store
  const {canDelete, name} = useMappedState(mapState, shallowEqual);

  // Create actions
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}

Usage

NOTE: React hooks require react and react-dom version 16.8.0 or higher.

StoreContext

Before you can use the hook, you must provide your Redux store via StoreContext.Provider:

import {createStore} from 'redux';
import {StoreContext} from 'redux-react-hook';
import reducer from './reducer';

const store = createStore(reducer);

ReactDOM.render(
  <StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>,
  document.getElementById('root'),
);

You can also use the StoreContext to access the store directly, which is useful for event handlers that only need more state when they are triggered:

import {useContext} from 'react';
import {StoreContext} from 'redux-react-hook';

function Component() {
  const store = useContext(StoreContext);
  const onClick = useCallback(() => {
    const value = selectExpensiveValue(store.getState());
    alert('Value: ' + value);
  });
  return <div onClick={onClick} />;
}

useMappedState(mapState, equalityCheck?)

Runs the given mapState function against your store state, similar to mapStateToProps. Unlike mapStateToProps, however, the result of your mapState function is compared for reference equality (===) by default. To use shallow equal comparison, pass in a comparision function as the second parameter.

const state = useMappedState(mapState);

You can use props or other component state in your mapState function. It must be memoized with useCallback, because useMappedState will infinitely recurse if you pass in a new mapState function every time.

import {useMappedState} from 'redux-react-hook';

function TodoItem({index}) {
  // Note that we pass the index as a dependency parameter -- this causes
  // useCallback to return the same function every time unless index changes.
  const mapState = useCallback(state => state.todos[index], [index]);
  const todo = useMappedState(mapState);

  return <li>{todo}</li>;
}

If you don't have any inputs (the second argument to useCallback) pass an empty array [] so React uses the same function instance each render. You could also declare mapState outside of the function, but the React team does not recommend it, since the whole point of hooks is to allow you to keep everything in the component.

The second parameter to useMappedState is used to determine if a new result from the mapState function is the same as the previous result, in which case your component will not be re-rendered. Prior to v4.0.1, this was hard-coded to a shallow equality check. Starting in v4.0.1, equalityCheck defaults to reference equality (using ===). To restore the old behavior, which is particularly useful when you are returning an object, you can use the shallowequal module:

import {useMappedState} from 'redux-react-hook';
import shallowEqual from 'shallowequal';

function TodoItem({index}) {
  // Note that we pass the index as a dependency parameter -- this causes
  // useCallback to return the same function every time unless index changes.
  const mapState = useCallback(
    state => ({
      todo: state.todos[index],
      totalCount: state.todos.length,
    }),
    [index],
  );
  const {todo, totalCount} = useMappedState(mapState, shallowEqual);

  return <li>{todo}</li>;
}

To avoid specifying the comparison function on every call to useMappedState, you can provide the defaultEqualityCheck option to create(). The shallowEqual function from fast-equals is another good option, as it handles shallow comparisons of Maps and Sets as well as objects.

NOTE: Every call to useMappedState will subscribe to the store. If the store updates, though, your component will only re-render once. So, calling useMappedState more than once (for example encapsulated inside a custom hook) should not have a large performance impact. If your measurements show a performance impact, you can switch to returning an object instead.

useDispatch()

Simply returns the dispatch method.

import {useDispatch} from 'redux-react-hook';

function DeleteButton({index}) {
  const dispatch = useDispatch();
  const deleteTodo = useCallback(() => dispatch({type: 'delete todo', index}), [
    index,
  ]);

  return <button onClick={deleteTodo}>x</button>;
}

create(options?)

Creates an instance of Redux React Hooks with a new StoreContext. The above functions are just exports of the default instance. You may want to create your own instance if:

  1. You want better type safety without annotating every callsite. Creating your own instance ensures that the types are the same for all consumers. See the example for more info.
  2. You want to provide a default implementation of equalityCheck for all calls to mapState
  3. You have multiple Redux stores (this is not common)
// MyStoreHooks.js

import {create} from 'redux-react-hook';

export const {StoreContext, useDispatch, useMappedState} = create();
// MyStoreHooks.ts

import {create} from 'redux-react-hook';

// Example in TypeScript where you have defined IState and Action
export const {StoreContext, useDispatch, useMappedState} = create<
  IState,
  Action,
  Store<IState, Action>
>();

create takes an optional options object with the following options:

  • defaultEqualityCheck - the default implementation of equalityCheck to use in useMappedState, defaults to refence equality (===)

To restore the pre v4.0.1 comparison behavior, for example:

import {create} from 'redux-react-hook';
import shallowEqual from 'shallow-equal';

// Example in TypeScript where you have defined IState and Action
export const {StoreContext, useDispatch, useMappedState} = create<
  IState,
  Action,
  Store<IState, Action>
>({defaultEqualityCheck: shallowEqual});

Example

You can try out redux-react-hook right in your browser with the Codesandbox example.

To run the example project locally:

# In one terminal, run `yarn start` in the root to rebuild the library itself
cd ./redux-react-example
yarn start

# In another terminal, run `yarn start` in the `example` folder
cd example
yarn start

FAQ

Is it typed with TypeScript/Flow?

One of the nice benefits of using hooks is that they are easier to type and less prone to trouble than higher-order components, especially when you are using multiple hooks (vs multiple HOCs). redux-react-hook comes with both TypeScript definitions and Flow types, both of which should work out of the box when installing with npm/yarn.

How does this compare to React Redux?

redux-react-hook has not been battle and perf-tested, so we don't recommend replacing react-redux just yet. React Redux also guarantees that data flows top down, so that child components update after their parents, which the hook does not.

How do I fix the error "Too many re-renders. React limits the number of renders to prevent an infinite loop."

You're not memoizing the mapState function. Either declare it outside of your stateless functional component or wrap it in useCallback to avoid creating a new function every render.

How can I use a selector creator, like in reselect?

If you want to share a selector with props across multiple component instances, create the selector in useMemo to ensure it has one copy per instance and use it directly in useMappedState.

function TodoList({listID}) {
  // useMemo will execute the function makeGetVisibleTodos once per component
  const getVisibleTodos = useMemo(makeGetVisibleTodos, []);
  const todos = useMappedState(
    useCallback(
      // Note that you shouldn't pass the entire props list, since every time
      // useCallback is recreated, useMappedState will resubscribe
      state => getVisibleTodos(state, {listID}),
      [listID, getVisibleTodos],
    ),
  );
}

Related projects

Here are some other projects that are adding hooks for Redux:

Thanks

Special thanks to @sawyerhood and @sophiebits for writing most of the initial implementation! This repo was setup with the help of the excellent create-react-library.

Contributing

Contributions are definitely welcome! Check out the issues for ideas on where you can contribute. See the CONTRIBUTING.md file for more details.

License

MIT © Facebook Inc.

redux-react-hook's People

Contributors

cpojer avatar dependabot[bot] avatar gaearon avatar goloveychuk avatar ianobermiller avatar mrtenz avatar msuhov avatar pauldijou avatar pinyin avatar richardpj avatar rickbeerendonk avatar stevenla avatar turanchoks avatar xinkule avatar zebratt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-react-hook's Issues

question about mutability of returns from useMappedState

Hi thank you very much for this library, its working really nicelly for us.

I have a small question just to confirm if this is expected behavior
For the basic scenario, i.e:

...

const mapState = useCallback(() => {
    return {
      mappedQuery: getQueryByName(requestId, queryName, store.getState())
    }
  }, [])

  let { mappedQuery } = useMappedState(mapState)

return mappedQuery 

...

Now component thats using that hook can updated the returned query directly, i.e.
mappedQuery.fieldName = "XXXX"
If I display the store state immediatelly after that, I see that the change is reflected directly there.

Is that the expected behavior, and if so, what is the recommended way to ensure all the updates are actually going through dispatch? Should we return a deep copy of the object from the custom hook?

Stale lastRenderedDerivedState

Context

Consider we have a store with a property called submitting and an action called setSubmitting.

Now imagine our UI, in a matter of a few milliseconds, dispatches setSubmitting(true), then it checks for errors in the form which causes immediately dispatching setSubmitting(false).

Problematic behavior

Apparently the current implementation of useMappedState shows the following behavior:

  • We start with lastRenderedDerivedState containing submitting: false. ✔️
  • checkForUpdates is called because we dispatched setSubmitting(true). ✔️
  • setDerivedState is invoked because lastRenderedDerivedState.current.submitting is false, but newDerivedState.submitting is true. ✔️
  • checkForUpdates is called because we dispatched setSubmitting(false). ✔️
  • setDerivedState is NOT INVOKED because, despite newDerivedState.submitting being false, lastRenderedDerivedState.current.submitting is also false (because it would only be updated after re-render and no re-render happened in this brief period). ❌

So at the end our component using useMappedState is only re-rendered once with submitting: true. The second render that would bring submitting: false doesn't happen.

Proposed solution

Don't wait until next re-render to update lastRenderedDerivedState. Simply update it right after setDerivedState is called. Also, change the name to lastDerivedState to make sense.

[Question] Why multi store subscriptions?

Just for curiosity, why you've opted for a multi-store subscription model instead of subscribing to the store in a top-level Provider?

Something like:

const ReduxStateContext = React.createContext()

function ReduxStateProvider({ store, children }) {
  const [state, setState] = React.useState(() => store.getState())

  useEffect(() => {
    let willUnsubscribe = false

    const checkForUpdates = () => {
      if (willUnsubscribe) return

      setState(prevState => {
        const nextState = store.getState()
        return shallowEqual(prevState, nextState) ? prevState : nextState
      })
    }

    checkForUpdates()

    const unsubscribe = store.subscribe(checkForUpdates)

    return () => {
      willUnsubscribe = true
      unsubscribe()
    }
  }, [store])

  return (
    <ReduxStateContext.Provider value={state}>
      {children}
    </ReduxStateContext.Provider>
  )
}

export function useMappedState(mapState) {
  const state = useContext(ReduxStateContext)

  const [derivedState, setDerivedState] = useState(() => mapState(state))

  useEffect(() => {
    setDerivedState(prevDerivedState => {
      const nextDerivedState = mapState(state)
      return shallowEqual(prevDerivedState, nextDerivedState)
        ? prevDerivedState
        : nextDerivedState
    })
  }, [state])

  // It might not even need useEffect() 🤔 (getDerivedStateFromProps)
  setDerivedState(prevDerivedState => {
    const nextDerivedState = mapState(state)
    return shallowEqual(prevDerivedState, nextDerivedState)
      ? prevDerivedState
      : nextDerivedState
  })

  return derivedState
}

Only the ReduxStateProvider subscribes to store updates, then it passes the update down to all consumers. Consumers have a chance to bail out by comparing prevDerivedState with nextDerivedState.

another crazy thought: reimplementation of Redux reselect.. built on top of this library

Hi.. it's me again :)

This issue is unrelated to my previous issue #78.. where I introduced a little library that is a light-weight wrapper around the redux-react-hook library to add an enhancement to useDispatch().

This issue pertains to another enhancement that I added to that library, which is a function called createReduxSelector which aims to provide a (very nearly) drop-in replacement for Redux reselect.

Links to learn more..

I just wanted to let you know about it..

  • it's already extremely efficient
    • there's always room for improvement, and I'm very open to ideas/suggestions
  • is this something you'd consider including in the upstream redux-react-hook library?

How to chain actions?

export const editSection = (section) => ({
  type: ACTION_TYPES.EDIT_SECTION,
  payload: section
});

export const openModal = () => ({
  type: ACTION_TYPES.OPEN_MODAL
});

export const closeModal = () => ({
   type: ACTION_TYPES.CLOSE_MODAL  
});

How can I make it so that when I call editSection it dispatches openModal? I was just going to do dispatch(openModal) but that requires useDispatch which can only be declared inside a react function as far as I'm aware.

testing with jest

Are there any plans to provide mocks for the store/ dispatch functions please?
Can not find any examples of how to test components which connect to the store via redux-react-hooks

Thanks

Custom comparator

Per #74, I propose we release a new major version of redux-react-hook in which you pass in your own equality comparator. That way you won't be tied to a particular comparison implementation.

ReactDOM peer dependency

Does ReactDOM need to be a peer dependency for this? I can only see it being used in a test file. I've been using this library in a react-native app and so far it has been fine apart from the npm install console warning which I just ignore for now.

ts error in example

TSlint is throwing error at line[36, 60] ./example/src/Store.ts per below

'Untyped function calls may not accept type arguments.ts(2347)'

Should this already resolved by the following commit?
d68d7a1

Would it be possible to use redux-form with StoreContext.Provider from redux-react-hook?

The following code results in: Invariant Violation: Could not find "store" in the context of "Connect(t)". Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(t) in connect options.

import {StoreContext} from 'redux-react-hook';
.
.
<StoreContext.Provider value={store}>
    <ReduxFormComponent />
</StoreContext.Provider>

However, this works fine.

import { Provider } from 'react-redux';
import {StoreContext} from 'redux-react-hook';
.
.
const store = useContext(StoreContext);
        
<Provider store={store}>
    <ReduxFormComponent />
</Provider>

redux-form seems to depend on react-redux's Provider. What would be the best way around this?

useDebugValue

Should we expose the derived state via useDebugValue? Could be useful for debugging, and we'd only format it when it needs to be displayed using the function as second argument.

question about re-render guarantee mentioned in docs

React Redux also guarantees that data flows top down, so that child components update after their parents, which the hook does not.

What does it means it doesn't flows top down. re-renders work fine for children. In which cases this guarantees is broken?

useMappedState should have selectors as dependencies

When state is mapped to an object or array like:

const getUsers = useCallback(
    state => {
        const ids = state.userIds;
        return ids.map(id => state.userStore[id]);
    },
    []
);
const users = useMappedState(getUsers);

The returning users can be a new array instance each time this component renders, passing users to a sub component could cause a re-render even userIds and userStore are not modified.

I'd rather to provide a more reselect like API:

const getUsers = useCallback(
    (ids, userStore) => ids.map(id => userStore[id]),
    []
);
const users = useMappedState(
    getUsers,
    [state => state.userIds, state => state.userStore]
);

In which the values returned by the second argument become dependencies to a useMemo call.

tslint keeps yelling

Usage
import { useMappedState } from "redux-react-hook";

Error - [ts] Cannot find module 'redux-react-hook'.

Tried removing node_modules and yarn install too but to no avail.

mapState function cannot return a function ?

When I'm using reselect along with redux-react-hook, there is a situation that needs the mapStateToProps function (mapState function in redux-react-hook) to return a function, but it seems this lib hasn't supported it yet ?

屏幕快照 2019-07-09 11 42 08

useMappedState returns incorrect value

Please see this CodeSandbox for simple redux app that has 2 inputs, that will dispatch a show/hide action on focus/blur event.

If you move the focus from the first input to the second, the the app will dispatch 2 actions & the reducer will update the store correctly (you can check it from redux dev tool)

However, the value returned from useMappedState is incorrect.

The interesting thing is, if I call useSelector hooks from react-redux then the return value of useMappedState is correct again. (You can try it by comment/uncomment line 35 of the code)

Another question is that is this project going to be deprecated in favour of built-in hooks from react-redux? Honestly I don't mind migrating to react-redux, but I couldn't find a way to make it work with multiple redux store

Readme useDispatch() example uses wrong import

In the useDispatch() code example in README.MD it isn't clear where useDispatch() is coming from. useMappedState() is imported but isn't used in the code example.

import {useMappedState} from 'redux-react-hook';

function DeleteButton({index}) {
  const dispatch = useDispatch();
  ...
}

React Devtools are crashing when selecting a Component using a redux-react hook.

I experienced crashes with my application when using the provided hooks.

When I try to inspect a component using the useMappedState hook, the application will crash with the following error:

image

image

My code implementation for the name component:

import React, { useCallback } from 'react'
import { useMappedState } from 'redux-react-hook'

const NamePreview = () => {
    const mapState = useCallback(state => ({
        name: state.name
    }), [])
    const { name } = useMappedState(mapState)

    return (
        <div>{ name }</div>
    )
}

export default NamePreview

looks like the devtools don't know how to interpret the value of the mapped state. Anyone else experienced issues like this?

How to use this with NextJS?

I'm trying to get this library to work on my NextJS project, but so far it's not working. I have set up Redux exactly like the NextJS example, for reference. That implementation works fine.

This is my _app.tsx component

<Container>
  <StoreContext.Provider value={reduxStore}>
    <Component {...pageProps} />
  </StoreContext.Provider>
</Container>

The error that I get is
redux-react-hook requires your Redux store to be passed through context via the <StoreContext.Provider>

Did anyone get this to work with NextJS yet? Or does anyone have an idea how it might work with NextJS / SSR?

Unsubscribed listeners might get notified

I came up with the exact same implementation of a hook for Redux and faced a problem that a redux listener is notified after it has unsubscribed.

The way I found it:
There is timer ticking every second and dispatching Date.now() to the store. And there are two components both subscribed to the store. One gets a timestamp from the store, the other gets derived data using a memoized (useCallback) selector which depends on a prop. If the prop changes the selector is recreated causing the listener bound to the previous selector to unsubscribe. However sometimes a listener is notified with a new state after it's unsubscribed. It happens because an ensureCanMutateNextListeners function in redux creates a temporary array of listeners while dispatching which may cause a wrong behaviour.

It's easier to demonstrate this with an example. In the example below there is also a very naive redux implementation. You can use it to see what's causing the problem.
https://codesandbox.io/s/3q0j74m0lq

Below is code from redux-react-hook showing the useEffect that causes a problem. One solution to the problem is to use useLayoutEffect so the re-subscription is scheduled right after the render.

useEffect(
  () => {
    let unsubbed = false;
    const checkForUpdates = () => {
      if (unsubbed) {
        // this shouldn't be reachable
      }

      const newDerivedState = runMapState();
      if (!shallowEqual(newDerivedState, lastRenderedDerivedState.current)) {
        setDerivedState(newDerivedState);
      }
    };
    checkForUpdates();
    const unsubscribe = store.subscribe(checkForUpdates);
    return () => {
      unsubscribe();
      unsubbed = true;
    };
  },
  [store, mapState]
);

I've spent quite some time figuring out how it should or should not work and I'm willing to help.

How to pass values from redux store to useCallback as dependency arguments

Hi Lads,
Sorry, if this is wrong place to ask the question, please point me to the correct one.
In my case I end up with situation when I have to update my property depend on the changes happened to one of the store values. Was looking through the tutorial and didn't find any solutions for this kind of cases.
After digging a little bit deeper into original redux api I found that I can subscribe to the store directly and getState() outside of useCallback, read the necessary value and provide it as a useCallback dependency argument. But in that case, seems like it makes the useMappedState() redundant...
Could you please help me to find the right way to sort this?

Investigate use of multiple useMappedState calls

The following note is in the README:

NOTE: You should only call useMappedState once per component, in the same way that you'd use connect from react-redux only once per component. Otherwise the component will have multiple store subscriptions and could rerender multiple times from a single store update.

I think this is a serious downside. One of the main benefits I would hope to get from using hooks with redux, is the ability to create a higher level abstraction. The purpose of doing this would be to hide the details of how my state is managed (i.e. that redux is used at all).

In the following example there are two custom hooks that use redux-react-hook under the hood. The actual component has no knowledge of Redux at all, it works entirely within its domain.

//
// Individual components
//
import {useDispatch, useMappedState} from 'redux-react-hook';

export function DeleteButton({index}) {
  let todo = useTodo(index)
  let user = useUser()
  
  return (
    <button 
      disabled={user.permissions.deleteTodos && !todo.canDelete} 
      onClick={todo.deleteTodo}
    >
      Delete {todo.name}
    </button>
  );
}

function useTodo(index) {
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  const {canDelete, name} = useMappedState(mapState);

  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return { name, canDelete, deleteTodo }
}

function useUser() {
  const mapState = useCallback(
    state => ({ user: state.user }),
    [],
  );

  const { user } = useMappedState(mapState);

  return user
}

Performance issues

the little change will make global render(update all component who used useMappedState function)

image

Question: Passing a store instance to the create function

Hi everyone,

I started experimenting with hooks on my application. I am super happy right now, it's a nice way of simplifying the code and reducing the amount of HOC and wrapper components. But I think we could go event further.
On my project, I am using typescript so I use the create function to have typed methods, it's quite handy, but I was wondering if we could maybe pass the store instance to the create method to get rid of the whole provider thing (thus getting rid of another wrapper).

The idea is quite simple, passing a store from the create function to the context as default value, so that we don't need the provider anymore.

// ...

export function create<
  TState,
  TAction extends Action,
  TStore extends Store<TState, TAction>
>(store: TStore | null = null): {
  StoreContext: React.Context<TStore | null>;
  useMappedState: <TResult>(mapState: (state: TState) => TResult) => TResult;
  useDispatch: () => Dispatch<TAction>;
} {
  const StoreContext = createContext<TStore | null>(store);

  // ...

}

Is there any reason why it was not done? Is it a bad idea ?

sometime async dispatch will not update by finally

const Loading = () => {
  const { requestCount } = useMappedState(state => state.framework);
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(actions.addRequestCount());
    setTimeout(() => {
      dispatch(actions.subRequestCount());
    }, 10);
  }, []);
  return (
    <div className={cx(styles.container, { [styles.showLoading]: requestCount > 0 })}>
      <img src={loadingIcon} alt="loading" />
    </div>
  );
};

and console in
image

work success console
image

work fail console
image

How to make this works with ConnectedRouter ?

The ConnectedRouter require a <Provider> in the root,
However, the redux-react-hook is using context to replace it.

I have try to use

<StoreContext.Provider value={store}>
    <Provider store={store}>
      <ConnectedRouter history={browserHistory}>
        <Switch>
          ...
        </Switch>
      </ConnectedRouter>
    </Provider>
  </StoreContext.Provider>

But looks not works perfectly

Reducing components rerenders

Looks like we can reduce component rerenders count by removing useEffect from here
https://github.com/facebookincubator/redux-react-hook/blob/676a270be12f0bcbffe9d650ef769d6dd486ef70/src/create.ts#L91-L94
Since useEffect will execute somewhen in the future after component renders, even if our hook already returned new state,
https://github.com/facebookincubator/redux-react-hook/blob/676a270be12f0bcbffe9d650ef769d6dd486ef70/src/create.ts#L110
still compares to old.

In practice, for my case, with using 6 hooks and 4 dispatches, it's reducing rerenders counts from 6 to 4.

here is used useLayoutEffect
https://github.com/reduxjs/react-redux/blob/06a674e733454b08bb4090f8bcb038a3a003e6aa/src/hooks/useSelector.js#L79-L85
Which seems to work correctly too.

High level API discussion: do we even need useMappedState

@gaearon and I were discussing a mismatch in the model of hooks and react-redux. To quote some of our conversation:

If I'm honest, the whole useMappedState API seems flawed to me.

Redux API has mapStateToProps because it literally gives you ... props. But in Hooks you don't have a layer in the middle. You're already in render. There's no props to give.

How much does it gain you compared to direct non-mapped useStoreState()?

The idea is that useMappedState is hard to implement efficiently (it has grown pretty ugly already) and easily because it is really trying to recreate much of what React hooks already do.

The example code would basically use components as containers and selectors, similar to connect:

export default function DeleteButtonContainer({index}) {
  // Fetch state and subscribe to the store
  const state = useStoreState();

  // Map your store state to what you need, using `useMemo` or selectors
  const stateProps = useMemo(() => state.todos[index], [state, index]);

  // Callbacks
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () => dispatch({type: 'delete todo', index}),
    [dispatch, index],
  );

  return <DeleteButton {...stateProps} {...{deleteTodo}} />;
}

const DeleteButton = React.memo(({canDelete, deleteTodo, name}) => {
  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
})

And useStoreState could look something like (stripped down for brevity):

export function useStoreState<TState>(): TState {
  const store = useContext(StoreContext);
  const [storeState, setStoreState] = useState(() => store.getState());

  useEffect(
    () => {
      let didUnsubscribe = false;

      const checkForUpdates = () => !didUnsubscribe && setStoreState(store.getState());

      checkForUpdates();

      const unsubscribe = store.subscribe(checkForUpdates);

      return () => {
        didUnsubscribe = true;
        unsubscribe();
      };
    },
    [store]
  );

  return storeState;
}

Basically it just passes the entire state to the React component.

If you wanted to put them all in a single component:

export default function DeleteButton({index}) {
  // Fetch state and subscribe to the store
  const state = useStoreState();

  // Map your store state to what you need, using `useMemo` or selectors
  const {canDelete, name} = useMemo(() => state.todos[index], [state, index]);

  // Callbacks
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () => dispatch({type: 'delete todo', index}),
    [dispatch, index],
  );

  // React will bail if the return value is reference equal
  return useMemo(() => (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  ), [canDelete, deleteTodo, name]);
}

Or if you want one container and multiple children:

function Parent() {
  const state = useStoreState()
  return (
    <>
      <Child val={selectX(state)} />
      <Child val={selectY(state)} />
    </>
  )
}

const Child = React.memo(function Child({val}) {
  return <div>{val}</div>;
});

UseMappedState firing continuously and re-rendering the component

Since the question statement makes it clear about problem statement, I'll jump right to code

const MyComponent = ({}) => {

    const mapState = useCallback(state => state.salesPerfReducer.salesPerf, []);
    const salesPerfData = useMappedState(mapState);
    console.log('component rendering...')
    // My custom hooks ( Nothing wrong here ...)
    const [tabIndex, handleTabChange] = useTab(0);
    const [duration, handleDurationChange] = useDuration('last3');
    
    const dispatch = useDispatch();
    useEffect( () => {
        console.log('fired')
        dispatch(fetchSalesPerf({ filter: 'monthly', duration: 'last3' })); // This is an API call and uses redux-observable for handling that async part
    }, [tabIndex, duration]);
   
    return (
        <section>
            Component
        </section>
    )
}

I see that my console.log is updating continuously.

When I tried commenting

 const mapState = useCallback(state => state.salesPerfReducer.salesPerf, []);
 const salesPerfData = useMappedState(mapState);

it only logs once. i.e. this statement console.log('component rendering...') is fired once.

So I'm using [] for useCallback which will fire every time as no dependencies are specified. But I'm not sure what to pass in as dependencies here. I tried [tabIndex, duration] but it was same.

First, checking this dependency could be built-in to useMappedState. Otherwise one would have to manually specify.

Taking this example, I don't want to run my API again unless my api Data is changed. Should be implicit.

Even if not for now. What's the way to make it know about that. I see in docs's example that you use a prop index in your component to pass to useCallback. But how can I prevent this here?

dispatch firing multiple times inside useEffect hook

Say I have to use dispatch inside useEffect on initial render and then whenever my two state values changes which are basically user action based ( Equivalent of cDM and cDU) and I code it like this

useEffect(
        () => {
            dispatch(fetchSalesPerf({ filter: 'monthly', duration: 'last3' }))
        },
        [tabIndex, duration ]
    );

Shouldn't this be a single dispatch call firing a single network request?

What I see is that I have multiple API calls for same endpoint flooding my network tab.

I don't have any setInterval in my action creators or epics. But still they go on.

Any way to mitigate this. I'm linking an issue which is related to this. However, I know that source of trouble is dispatch and not useMappedState. What are your thoughts ?

#18

Make useMappedState take a memoization array,

It's pretty weird that useMappedState requires the input function to be pre-memoized (or be extracted out of the calling function.

useMappedState should work like the other hooks and should take a second argument of an array to memoize with.

This way, the hook would ignore if the function changes, and will only re-subscribe if any of the values passed in the second argument changes.

So this:

const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // Get data from and subscribe to the store
  const {canDelete, name} = useMappedState(mapState);

would become :

const {canDelete, name} = useMappedState(
 state => ({
    canDelete: state.todos[index].canDelete,
    name: state.todos[index].name,
  }),
  [index],
);

Using function defined outside the function etc would continue to work. Further, if no second argument is provided, we can default to the value of the reference of the function provided and the change would remain backwards compatible.

Hooks can only be called inside the body of a function component.

include react with

<script src="/js/lib/react.production.min.js"></script>
<script src="/js/lib/react-dom.production.min.js"></script>

include redux-react-hook with webpack
import {StoreContext,useMappedState,useDispatch} from 'redux-react-hook';

Error:Hooks can only be called inside the body of a function component.

i find node_modules/redux-react-hook/dist/index.js has code

var react = require('react');
i think this cause the error

I resolve this with:copy redux-react-hook/dist/index.js to my project and modify

var react = React;//from window.React

so, i want ask has a better way?

Equivalent for mapStateToProps's `ownProps`

This evening I was playing with this api, and I got myself in the need of using some props of my functional component to fetch some state.

The problem:

import { useMappedState } from 'redux-react-hook'

const mapState = (state) => ({
  selectedProfile: state.profiles[somePropsHere] // how do I pass` somePropsHere`?
})

export default ({ somePropsHere }) => {
  const { selectedProfile } = useMappedState(mapState)

I come out with a quick solution:

import { useCallback } from 'react'
import { useMappedState as useReduxMappedState } from 'redux-react-hook'

export function useMappedState(mapState, additionalProps) {
  const wrappedMapState = useCallback((state) => mapState(state, additionalProps), [])
  return useReduxMappedState(wrappedMapState)
}

Which allow you to forward props from the component to the map state callback:

const mapState = (state, { somePropsHere }) => ({
  selectedProfile: someSelectorThatUseProps(somePropsHere, state),
})

export default ({ somePropsHere }) => {
  const { selectedProfile } = useMappedState(mapState, { somePropsHere })

Since this is a replacement for react-redux, is it worth to send a PR to allow this kind of behaviour? Or is there a better alternative?

do you have multiple reducers example?

example just have one reducer with one state.

export const {StoreContext, useDispatch, useMappedState} = create<
  IState,
  Action,
  Store<IState, Action>
>();

if have multiple reducers, the IState and IAction is like this?

export interface IState {
  state1: IState1;
  state2: IState2;
}

export interface IState2 {
  lastUpdated: number;
  todos: string[];
}

export interface IState1 {
  lastUpdated: number;
  todos: string[];
}

export type Action =
  | {
      type: 'add todo';
      todo: string;
    }
  | {
      type: 'delete todo';
      index: number;
    }
  | {
      type: 'add todo2';
      todo: string;
    }
  | {
      type: 'delete todo2';
      index: number;
    }
;

crazy thought: useDispatch() returns wrapper that invokes dispatch() on Redux store as well as any number of React.useReducer() state mutators

I don't know if this would be considered an anti-pattern.

Redux seems like the right place to store state that needs to be shared with other React components. But if for some reason an app chooses to store some bit of state in a React component and then needs to pass callbacks to children, it is likely to follow the recommend convention to pass down a dispatch function from useReducer via context.

In this scenario, it would be convenient to be able to merge Redux and React dispatch functions.

pseudo code:

import {useReducer} from 'react';
import {useDispatch} from 'redux-react-hook';

function todosReducer(todosState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...todosState, action.payload];
    default:
      return todosState;
  }
}

function TodosApp() {
  const [dispatch, addDispatcher] = useDispatch();  // could omit `dispatch` from destructured Array
  const [todos, todosDispatcher] = useReducer(todosReducer, []);
  addDispatcher(todosDispatcher);

  return (<DeepTree todos={todos} />);
}
import {useDispatch} from 'redux-react-hook';

function DeepChild(props) {
  const [dispatch, addDispatcher] = useDispatch();  // could omit `addDispatcher` from destructured Array

  function handleClick() {
    dispatch({ type: 'ADD_TODO', payload: 'buy milk' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

implementation could be as simple as:

in file create.ts:

change from:

  function useDispatch(): Dispatch<TAction> {
    const store = useContext(StoreContext);
    if (!store) {
      throw new MissingProviderError();
    }
    return store.dispatch;
  }

to:

  const reactDispatchers = [];

  const addDispatcher = (dispatch) => reactDispatchers.push(dispatch);

  function useDispatch(): Dispatch<TAction> {
    const store = useContext(StoreContext);
    if (!store) {
      throw new MissingProviderError();
    }

    const combinedDispatcher = (action) => {
      ;[...reactDispatchers, store.dispatch].forEach(dispatch => dispatch(action))
    }

    return [combinedDispatcher, addDispatcher];
  }

Seamless gradual upgrade from react-redux

Related to #3

Edit: This will not work because react-redux injects state but redux-react-hooks injects store into Context

If redux is same, then reusing context should be as easy as injecting foreign context object:

https://github.com/facebookincubator/redux-react-hook/blob/3a86bc02ca81def12e0240c986d56ee1da5fc792/src/create.ts#L21-L30

new code (will not work)
export function create< 
  TState, 
  TAction extends Action, 
  TStore extends Store<TState, TAction> 
>(customContext?: React.ContextType): { 
  StoreContext: React.Context<TStore | null>; 
  useMappedState: <TResult>(mapState: (state: TState) => TResult) => TResult; 
  useDispatch: () => Dispatch<TAction>; 
} { 
  const StoreContext = customContext || createContext<TStore | null>(null); 

  // ...
}

This may break if context shapes from react-redux and redux-react-hook are different — they are different

Cannot use with TypeScript, because types are missing in package.json

The types are deployed to NPM, but the types field in the package.json is not set, so TypeScript doesn't pick up the types and I'm unable to use it in my TypeScript project.

error TS2305: Module '"node_modules/redux-react-hook/dist"' has no exported member 'StoreProvider'.

This can be fixed by setting "types": "dist/index.d.ts" in package.json.

How to use with Router? E.g. connected-react-router?

I'm using redux-react-hook with react-router-dom which works fine. Now I want to connect the router to Redux, such that it dispatches actions when I'm routing and give me the ability to route using actions.

Previously I'd use connected-react-router for that, but that's dependent on react-redux.

Is this possible?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.