Code Monkey home page Code Monkey logo

redux-hooks's Introduction

⚓ @epeli/redux-hooks

React Hooks implementation for Redux that does not suffer from the tearing / "zombie child component" problems.

UPDATE Nope. This is buggy too. And so is every other Redux Hooks lib.

It also implements performance optimizations eg. does not render when the map state function does not produce new value and allows advanced optimizations with memoizing and dependency arrays.

Written in TypeScript so types are baked in and always up to date.

📦 Install

npm install @epeli/redux-hooks

📖 Usage

import {useMapState, useActionCreators} from "@epeli/redux-hooks";

const ActionCreators = {
    inc() {
        return {type: "INCREMENT"};
    },
};

function Counter() {
    const count = useMapState(state => state.count);
    const actions = useActionCreators(ActionCreators);

    return <button onClick={actions.inc}>{count}</button>;
}

Your components must be wrapped with the HooksProvider

import {HooksProvider} from "@epeli/redux-hooks";

ReactDOM.render(
    <HooksProvider store={store}>
        <Counter />
    </HooksProvider>,
    document.getElementById("app"),
);

Custom provider is required for now because the official react-redux bindings do not use subscriptions and it's impossible to implement Redux hooks without efficiently. Read more about it here.

📚 Available hooks

  • useMapState() Renders when returned value differ using Object.is() check
  • useSelect() Renders when returned value differ using shallow equal check
  • useActionCreators() Bind object of action creators to dispatch
  • useDispatch() Returns the plain dispatch-function
  • usePassiveMapState() Like useMapState() but does not subscribe to the store eg. is executed only when the component renders.

Please read the optimizations docs for details when to use these.

TypeScript usage

You can use createHooks() factory to create custom typed version of the hooks

import {createHooks} from "@epeli/redux-hooks";

const AppHooks = createHooks<{foo: string}>();

function Foo() {
    const foo = AppHooks.useMapState(state => state.foo);
    return <div>String: {foo}</div>;
}

Examples

Codesandbox: https://codesandbox.io/s/github/epeli/typescript-redux-todoapp/tree/hooks

Github: https://github.com/epeli/typescript-redux-todoapp/tree/hooks

🤔 Why yet another Redux Hooks implementation?

All the others I checked had the zombie child bug, poor performance or were missing TypeScript types.

Even the facebookincubator/redux-react-hook one has the zombie bug which is stated in their FAQ. This one guarantees data flow top down like the official react-redux one does.

This also an experiment for the future of the react-redux:

reduxjs/react-redux#1177 (comment)

redux-hooks's People

Contributors

elenabratanova avatar esamattis avatar jarvisaoieong 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

Watchers

 avatar  avatar  avatar  avatar  avatar

redux-hooks's Issues

Component does not receive updates in Jest unit test using react-test-renderer

After finding a work around for my type error in #3 I am now attempting to test a component written using react-hooks and it appears that the component does not receive the updates.

The basic setup is: TypeScript 3.3.3, react-hooks 0.5.0, react 16.8.1, jest 23.x, redux 4.0.1, immer-reducer 0.6.0

What doesn't work, is a component like this:

import { useActionCreators } from "@epeli/redux-hooks";
import React from "react";

import { ActionCreators } from "./state";
import { useSearchState } from "./store";

export const Search: React.SFC<{}> = () => {
  const { loading, query } = useSearchState((state) => state);
  const actions = useActionCreators(ActionCreators);

  return (
    <div>
            <h2>{query}</h2>
            <button onClick={actions.loadProcedures}>Load</button>
            <p>{loading ? "Loading" : ""}</p>
    </div>
  );
};

My test case looks like:

describe("", () => {
    it("should update loading on click using hooks", async (done) => {
        const store = createSearchStore();

        const component = create(
            <HooksProvider store={store}>
                <HookedSearch />
            </HooksProvider>,
        );

        const rootInstance = component.root;
        const text = rootInstance.findByType("p");
        expect(text.children[0]).toBe("");

        const button = rootInstance.findByType("button");
        button.props.onClick();

        // Timeout just there to see if it wasn't some sort of async update/timing problem
        setTimeout(() => {
            expect(store.getState().loading).toBe(true);
            expect(text.children[0]).toBe("Loading");  // <-- this fails!!
            done();
        }, 200);
    });
});

An almost identical test, using a more traditional component that uses react-redux 6.0.0 works correctly.

My suspicion is that something about the way react-hooks sets up its subscription to the store does not work correctly when running under the react-test-renderer

Is this a known problem?

Is there something else I need to be doing to get the update to work in tests?

The version of the component that does work looks like this:

import React from "react";
import { connect } from "react-redux";

import { ActionCreators, ProcedureSearchState } from "./state";

interface SearchProps {
    loading: boolean;
    query: string;

    loadProcedures: typeof ActionCreators.loadProcedures;
}

const Search: React.SFC<SearchProps> = (props) => {
    return (
        <div>
            <h2>{props.query}</h2>
            <button onClick={props.loadProcedures}>Load</button>
            <p>{props.loading ? "Loading" : ""}</p>
        </div>
    );
};

const mapStateToProps = (state: ProcedureSearchState) => ({
    loading: state.loading,
    query: state.query,
});

const mapDispatchToProps = { loadProcedures: ActionCreators.loadProcedures };

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(Search);

The only difference in the test is to use the react-redux <Provider> component instead of <HooksProvider>.

useMapState returns stale values when selecting data using useState values as dependecies

Hi!
Awesome library, thanks for sharing it.
I noticed that using useMapState to selected data that has a useState dependency returns a stale value from the state.
Not sure if it's an issue of the library or if I'm making a mistake, but here is a CodeSandbox that demonstrates the issue:
https://codesandbox.io/s/0yz3m6v4r0

In the code I'm dispatching a Redux action (setReduxValues) after 5 seconds after mounting the component. The action adds some values in the Redux state and is dispatched and captured by the reducer successfully.
To get the updated value from the Redux state i use useMapState with a selector that has a dependency on some internal component state of the component... the issue is that if you change the internal component state before the setReduxValues action is dispatched, the selector will still use the old internal component state dependency instead of the updated one.
Since the doc says:

Without the dependencies array the state is mapped always when the component renders. When the store state updates the map state function is executed regardless of the deps array.

I would expect the selector to use the new updated dependency 🤔 But maybe I'm missing something.

TLDR: In the CodeSandbox example, press the "Set letter to B" button asap and wait a few seconds.
You'll see that the printed value will be Redux value: IM THE VALUE OF A, but it should be Redux value: IM THE VALUE OF B
It will work correctly if instead of pressing the button asap you wait for at least 5 seconds.

Let me know if I can help in any other way!

State of store can change before the provider subscribes

Currently, there is a timing issue when children dispatch an action inside useEffect during their first render.
Here's the order of events:

  1. Some consumer reads the initial value via useMapState
  2. A child of the provider dispatches an action inside useEffect
  3. The provider itself subscribes to the store via useEffect

As a result, consumers are not updated with the new state value. Here's how react-redux handles this:
https://github.com/reduxjs/react-redux/blob/72ed6db1947913bdd4900b324243c43a490b4c5a/src/components/Provider.js#L56

Can useReduxState be used when mapStateToProps depends on component's own props?

@epeli many thanks for the lib! I've been playing with it today, and noticed this issue:
Example scenario: there's a cache of whatever in redux and we want to select data based on some id passed to component.
Like so: https://codesandbox.io/s/1qzzp84rq4 The User component gets an userId prop passed to it (which changes on button clicks) and pulls that user's name from redux. Currently it gets stuck with data for initial userId and ignores changes.

Am I doing something wrong? Is this by design?

[question] How do you enforce top-down updates?

I was admiring your work and all I have is just a quick question: How did you enforce top-down updates (aka zombie bug)?

I couldn't figure it out by looking at your code.

Anyway, thanks for the library! ❤️

great idea for the warning!

This few lines here:

if (process.env.NODE_ENV !== "production") {
                const check = mapStateRef.current(state);
                if (!is(check, res)) {
                    console.warn(
                        "useMapState returns new identity on every run. This causes the component to render on every Redux state change. Consider use using useSelect().",
                        res,
                    );
                }
            }

this is so so excellent. You have saved me so much time. It cought every single place I mistankely was returning new identities and/or in general misconfiguring stuff. I know you are sort of using this as a prototyping for official redux hooks, so I just want to encourage you that this sort of warning is also included there.

TS2322 type error using HooksProvider together with immer-reducer

@epeli Thanks for this an immer-reducer. The combination looks great and I hope for it to be able to reduce the boilerplate we have in a lot of our code.

I was trying this out today on a simple example, and I keep getting a TS2322 error on the line:

const store = createMyStore();   // Simple store with a single ImmerReducer-created render function

const renderApp = () => (
  <HookesProvider store={store}>    // <-- error here!!
    <MyStateUsingComponent />
  </HooksProvider>
);

The createMyStore function looks like this:

import {createUseMapState} from "@epeli/redux-hooks";
import { createStore } from "redux";
import { ProcedureSearchState, reducerFunction } from "./state";

export const useSearchState = createUseMapState<ProcedureSearchState>();

export const createSearchStore = () => createStore(reducerFunction);

The renderFunction and ProcedureSearchState look like this:

import { createActionCreators, createReducerFunction, ImmerReducer } from "immer-reducer";

export interface Procedure {
    readonly id: string;
    readonly name: string;
}

export interface ProcedureSearchState {
    readonly loading: boolean;
    readonly procedures: Procedure[];
    readonly query: string;
}

export const initialState: ProcedureSearchState = {
    loading: false,
    procedures: [],
    query: "",
};

export class ProcedureSearchReducer extends ImmerReducer<ProcedureSearchState> {
    public setQuery(query: string) {
        this.draftState.query = query;
    }

    public loadProcedures() {
        this.draftState.loading = true;
    }

    public receiveProcedures(procedures: Procedure[]) {
        this.draftState.procedures = procedures || [];
        this.draftState.loading = false;
    }
}

export const ActionCreators = createActionCreators(ProcedureSearchReducer);
export const reducerFunction = createReducerFunction(ProcedureSearchReducer, initialState);

The full error message is:

src/index.tsx:9:20 - error TS2322: Type 'Store<ProcedureSearchState, { type: "setQuery"; payload: string; } | { type: "loadProcedures"; payload: []; } | { type: "receiveProcedures"; payload: Procedure[]; }>' is not assignable to type 'Store<any, AnyAction>'.
  Types of property 'dispatch' are incompatible.
    Type 'Dispatch<{ type: "setQuery"; payload: string; } | { type: "loadProcedures"; payload: []; } | { type: "receiveProcedures"; payload: Procedure[]; }>' is not assignable to type 'Dispatch<AnyAction>'.
      Type 'AnyAction' is not assignable to type '{ type: "setQuery"; payload: string; } | { type: loadProcedures"; payload: []; } | { type: "receiveProcedures"; payload: Procedure[]; }'.
        Type 'AnyAction' is not assignable to type '{ type: "receiveProcedures"; payload: Procedure[]; }'.

9     <HooksProvider store={store}>
                     ~~~~~

  node_modules/immer-reducer/lib/immer-reducer.d.ts:50:9
    50         payload: FirstOrAll<Payload>;
               ~~~~~~~
    'payload' is declared here.
  node_modules/@epeli/redux-hooks/lib/redux-hooks.d.ts:18:5
    18     store: Store;
           ~~~~~
    The expected type comes from property 'store' which is declared here on type 'IntrinsicAttributes & { store: Store<any, AnyAction>; children: ReactNode; }'

I tried created a CodeSandox showing the error, but of course I don't get an error there.

I made sure I had the same tsconfig.json as your example here, but it made no difference. I still get the compiler error.

What am I doing wrong? What other information do you need to help me debug this?

The demos and the description of this look really great, and I would really like to try it out on a more substantial example once I get the very basics working.

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.