Code Monkey home page Code Monkey logo

react-tracked's Introduction

logo

React Tracked

CI npm size discord

State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.

Documentation site: https://react-tracked.js.org

Introduction

Preventing re-renders is one of performance issues in React. Smaller apps wouldn't usually suffer from such a performance issue, but once apps have a central global state that would be used in many components. The performance issue would become a problem. For example, Redux is usually used for a single global state, and React-Redux provides a selector interface to solve the performance issue. Selectors are useful to structure state accessor, however, using selectors only for performance wouldn't be the best fit. Selectors for performance require understanding object reference equality which is non-trival for beginners and experts would still have difficulties for complex structures.

React Tracked is a library to provide so-called "state usage tracking." It's a technique to track property access of a state object, and only triggers re-renders if the accessed property is changed. Technically, it uses Proxies underneath, and it works not only for the root level of the object but also for deep nested objects.

Prior to v1.6.0, React Tracked is a library to replace React Context use cases for global state. React hook useContext triggers re-renders whenever a small part of state object is changed, and it would cause performance issues pretty easily. React Tracked provides an API that is very similar to useContext-style global state.

Since v1.6.0, it provides another building-block API which is capable to create a "state usage tracking" hooks from any selector interface hooks. It can be used with React-Redux useSelector, and any other libraries that provide useSelector-like hooks.

Install

This package requires some peer dependencies, which you need to install by yourself.

yarn add react-tracked react scheduler react-dom

For React Native users:

yarn add react-tracked react scheduler react-native

Usage

There are two main APIs createContainer and createTrackedSelector. Both take a hook as an input and return a hook (or a container including a hook).

There could be various use cases. Here are some typical ones.

createContainer / useState

Define a useValue custom hook

import { useState } from 'react';

const useValue = () =>
  useState({
    count: 0,
    text: 'hello',
  });

This can be useReducer or any hook that returns a tuple [state, dispatch].

Create a container

import { createContainer } from 'react-tracked';

const { Provider, useTracked } = createContainer(useValue);

useTracked in a component

const Counter = () => {
  const [state, setState] = useTracked();
  const increment = () => {
    setState((prev) => ({
      ...prev,
      count: prev.count + 1,
    }));
  };
  return (
    <div>
      <span>Count: {state.count}</span>
      <button type="button" onClick={increment}>
        +1
      </button>
    </div>
  );
};

The useTracked hook returns a tuple that useValue returns, except that the first is the state wrapped by proxies and the second part is a wrapped function for a reason.

Thanks to proxies, the property access in render is tracked and this component will re-render only if state.count is changed.

Wrap your App with Provider

const App = () => (
  <Provider>
    <Counter />
    <TextBox />
  </Provider>
);

createTrackedSelector / react-redux

Create useTrackedSelector from useSelector

import { useSelector, useDispatch } from 'react-redux';
import { createTrackedSelector } from 'react-tracked';

const useTrackedSelector = createTrackedSelector(useSelector);

useTrackedSelector in a component

const Counter = () => {
  const state = useTrackedSelector();
  const dispatch = useDispatch();
  return (
    <div>
      <span>Count: {state.count}</span>
      <button type="button" onClick={() => dispatch({ type: 'increment' })}>
        +1
      </button>
    </div>
  );
};

createTrackedSelector / zustand

Create useStore

import create from 'zustand';

const useStore = create(() => ({ count: 0 }));

Create useTrackedStore from useStore

import { createTrackedSelector } from 'react-tracked';

const useTrackedStore = createTrackedSelector(useStore);

useTrackedStore in a component

const Counter = () => {
  const state = useTrackedStore();
  const increment = () => {
    useStore.setState((prev) => ({ count: prev.count + 1 }));
  };
  return (
    <div>
      <span>Count: {state.count}</span>
      <button type="button" onClick={increment}>
        +1
      </button>
    </div>
  );
};

Notes with React 18

This library internally uses use-context-selector, a userland solution for useContextSelector hook. React 18 changes useReducer behavior which use-context-selector depends on. This may cause an unexpected behavior for developers. If you see more console.log logs than expected, you may want to try putting console.log in useEffect. If that shows logs as expected, it's an expected behavior. For more information:

API

docs/api

Recipes

docs/recipes

Caveats

docs/caveats

Related projects

docs/comparison

https://github.com/dai-shi/lets-compare-global-state-with-react-hooks

Examples

The examples folder contains working examples. You can run one of them with

PORT=8080 yarn run examples:01_minimal

and open http://localhost:8080 in your web browser.

You can also try them in codesandbox.io: 01 02 03 04 05 06 07 08 09 10 11 12 13

Benchmarks

See this for details.

Blogs

react-tracked's People

Contributors

aarbi avatar dai-shi avatar daviseford avatar davo avatar dependabot[bot] avatar floer32 avatar fnpen avatar leolebras avatar raminious avatar sakurawen avatar scottawesome avatar seanblonien 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

react-tracked's Issues

New memo ignores prop definitions

I've noticed that with move to typescript while losing memo from react-tracked props types are not being passed through unless they are specified in callback arguments itself. I've tried fixing it myself but wasn't able to unfortunately, hence made this issue.

// Works
const MyComponent = memo(({ foo }: { foo: string }) => {})

// Doesn't, typeof foo is 'any'
const MyComponent: FC<{ foo: string }> = memo(({ foo }) => {})

Here is another example in sandbox:

Schermafbeelding 2021-01-14 om 13 14 58

You can try it out here: https://codesandbox.io/s/friendly-rhodes-n8rm8?file=/src/components/FilterLink.tsx

Pass initial state to useReducer

First and foremost, awesome idea with using Proxy to track conditional component updates.

How would I go about passing a dynamic initial state into useReducer? Something like the following:

const useValue = value => useReducer(store.reducer, value);

export function StoreProvider({ value, children }) {
    return <Provider useValue={() => useValue(value)}>{children}</Provider>;
}

In the above case I get:

useValue must be statically defined

Prop types are lost when using memo

I've noticed that with move to typescript while losing memo from react-tracked props types defined as generics to FC or VFC are not being passed through unless they are specified in callback arguments itself. I've tried fixing it myself but wasn't able to unfortunately, hence made this issue.

// Works
const MyComponent = memo(({ foo }: { foo: string }) => {})

// Doesn't, typeof foo is 'any'
const MyComponent: FC<{ foo: string }> = memo(({ foo }) => {})

Here is another example in sandbox:

Schermafbeelding 2021-01-14 om 13 14 58

You can try it out here: https://codesandbox.io/s/friendly-rhodes-n8rm8?file=/src/components/FilterLink.tsx

False negatives in tracking with React.memo

Just tried to imeplement ToDo app with useReducer pattern.
With React.memo, I found a huge limitation in the current implementation.

If tracking finds both todo object and todo.id are used, todo is checked for ref equality in memo, we have false negatives (no render).

More investigation needed. (Like why js-framework-benchmark is working.)


There are two issues.

  1. use a property in callback
onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
  1. use a property for key
<TodoItem key={todo.id} todo={todo} />

How to use react-tracked when initial data is fed by a HOC

Hi @dai-shi !

Thank you for that great library, just by reading your blog post, I learnt a lot about why I might be having performance issues in my app.

I'm not sure how to use react-tracked in my case.

Here's the context: I have some HOCs connected to my Redux store that I used to use all over the place to access data or use methods.

For commodity, I decided to create different Contexts at the top of my app, with initialValue coming from those HOCs. This way, my children components no longer need to be wrapped by HOCs, I just useWhateverNeeded from the contexts.

Here's a simplified version of what I do:

import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import useDeepCompareEffect from 'use-deep-compare-effect'
import { Context } from 'contexts'
import { WithCurrentPpmProps } from 'modules/current-ppm/components/withCurrentPpm'

const LayoutInner: React.FC<WithCurrentPpmProps > = (props) => {
	const [ currentPpm, setCurrentPpm ] = useState(props.currentPpm)

	// Update current PPM (instead of useEffect to avoid infinite loop)
	useDeepCompareEffect(
		() => {
			setCurrentPpm(props.currentPpm)
		},
		[ props.currentPpm ]
	)

	return (
		<Context.currentPpm.Provider value={{ currentPpm }}>
			<Layout {...newSiteLayoutProps} />
		</Context.currentPpm.Provider>
	)
}

export const Layout = withCurrentPpm(LayoutInner)

How would you use react-tracked when the initial context providers' value is fed by a HOC value that can change?

Thanks!

Comparison with related projects

Context value Using subscriptions Optimization for rendering big object Dependencies Package size
react-tracked state-based object Yes *1 Proxy-based tracking No 1.5kB
constate state-based object No No (should use multiple contexts) No 329B
unstated-next state-based object No No (should use multiple contexts) No 362B
zustand N/A Yes Selector function No 742B
react-sweet-state state-based object Yes *3 Selector function No 4.5kB
storeon store Yes state names No 337B
react-hooks-global-state state object No *2 state names No 913B
react-redux (hooks) store Yes Selector function Redux 5.6kB
reactive-react-redux state-based object Yes *1 Proxy-based tracking Redux 1.4kB
easy-peasy store Yes Selector function Redux, immer, and so on 9.5kB
mobx-react-lite mutable state object No *4 Proxy-based tracking MobX 1.7kB
hookstate N/A Yes Proxy-based tracking No 2.6kB
  • *1 Stops context propagation by calculateChangedBits=0
  • *2 Uses observedBits
  • *3 Hack with readContext
  • *4 Mutation trapped by Proxy triggers re-render

Shared actions in react-tracked?

Is it possible and how, to create actions in react-tracked folder structure?
Redux pattern?

Points are to create action and share them between components.
In the documentation, are action only in one component?

Is there any problem on example?

Hello @dai-shi . Firstly thanks for the repo.
I have problem about example on the README. I try to implement it on my React Native project but it looks like doesn't block the unnecessary render.

Here is my code (same as your's, just with native components):

import React, { useReducer } from 'react';
import { createContainer } from 'react-tracked';
import { TextInput, View, Text, Button, SafeAreaView } from 'react-native';

const useValue = ({ reducer, initialState }) => useReducer(reducer, initialState);
const { Provider, useTracked } = createContainer(useValue);

const initialState = {
    count: 0,
    text: 'hello',
};

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment': return { ...state, count: state.count + 1 };
        case 'decrement': return { ...state, count: state.count - 1 };
        case 'setText': return { ...state, text: action.text };
        default: throw new Error(`unknown action type: ${action.type}`);
    }
};

const Counter = () => {
    const [state, dispatch] = useTracked();
    console.log("RENDER COUNTER");
    return (
        <View>
            <Text>{Math.random()}</Text>
            <View>
                <Text>Count: {state.count}</Text>
                <Button title="+1" onPress={() => dispatch({ type: 'increment' })} />
                <Button title="-1" onPress={() => dispatch({ type: 'decrement' })} />
            </View>
        </View>
    );
};

const TextBox = () => {
    const [state, dispatch] = useTracked();
    console.log("RENDER BOX");
    return (
        <View>
            <Text>{Math.random()}</Text>
            <View>
                <Text>Text: {state.text}</Text>
                <TextInput value={state.text} onChangeText={event => dispatch({ type: 'setText', text: event })} />
            </View>
        </View>
    );
};

const App = () => (
    <Provider reducer={reducer} initialState={initialState}>
        <SafeAreaView>
            <Text>Counter</Text>
            <Counter />
            <Counter />
            <Text>TextBox</Text>
            <TextBox />
            <TextBox />
        </SafeAreaView>
    </Provider>
);

export default App

My output:
Simulator Screen Shot - iPhone 11 - 2020-07-06 at 17 29 01

When I click the counter button it's renders TextBox component too.
Screen Shot 2020-07-06 at 17 29 39

I believe it shouldn't render the other component. Am I wrong? Or is there any misuse?

can we use redux thunks with this?...

Hi, this project seems great but you didn't mention nothing about thunks and it's not clear if we can use middlewares here...would be possible use thunks for async operations with this lib?

thank you so much

How to have global state with recat-native-navigration

@dai-shi Thanks for this repo. On RNN each screen is a seperate tree, and so you need to which has it's own Provider. Like so:

Navigation.registerComponent(
    'Templates',
    () => props => (
      <Provider>
         <Templates {...props} />
      </Provider>
    ),
    () => Templates,
  );
  Navigation.registerComponent(
    'Editor',
    () => props => (
      <Provider>
         <Editor {...props} />
      </Provider>
    ),
    () => Editor,
  );

The problem is that each screen seem to have it's own context or state, how can I make sure the state is shared across screens?

Thanks!

How to use with Class components?

Is this limited to usage with functional components? I would like to integrate into an existing project that uses class components. Any examples of this?

Extra re-renders with useTracked question

I'm testing some state management options for Next.js project, and usually context+tracked is more than enough in my CRA apps.
Sadly it does not work with Next.js. All state based components are updated on any state change just like it would be clean react context only.

It's more like a question is it possible at all to make this work together or what I'm missing?

Unexpected behavior when map() is applied to the property of the tracked state

Greetings! I have an action in my reducer which map over an array in the store:

setMyArrayInStore: ({ payload, store }) => {
    const { rowData, type } = payload;

    return {
      myArrayInStore: store.myArrayInStore.map(
        (item) => {
          if (item.field !== rowData.field) {
            return item;
          }

          return {
            ...item,
            [type]: rowData[type],
          };
        }
      ),
    };
  },

In another part of my code, I'm using myArrayInStore as a useEffect() dependency.

const { myArrayInStore } = useTrackedState();

useEffect(() => {
  ...some actions on myArrayInStore change
}, [myArrayInStore], 

Without react-tracked useEffect() recognize that myArrayInStore was recreated and fires actions inside.

When I change the code to use react-tracked, useEffect() no longer fire actions.

Is it possible to use state outside of the React context?

In Redux, I can do something like:

import store from "./store";

store.dispatch({ type: "SOME_ACTION" });
const state = store.getState();

in whatever place I want. When using React Context, I'm kinda forced to use it only from within the component. There are ways of creating functions that get reference to dispatch function but still everything has to be done in the context of Provider.

I would like to use react-tracked but I guess it's currently impossible to switch from Redux when someone is using store outside of the React context? Right?

Best Practice of React Tracked / React Hooks

Hi, It's me again :)

I dig on your example and found about form example . And I wonder how to keep the state up to latest state without have "button submit" re-render. And I have tried to separate the into new component, but still no luck. So can react hooks / react-tracked solve this rerender case ?

/* eslint react/jsx-props-no-spreading: off */

import React from 'react';

import {
  FormProvider,
  useFormValues,
  useFormError,
  useFormField,
} from './form';

const validateName = (name: unknown) => {
  if (typeof name !== 'string') return new Error('invlid type');
  if (name.length === 0) return new Error('name is required');
  return null;
};

const FirstName: React.FC = React.memo(() => (
  <div>
    {Math.random()}
    <div>
      First Name:
      <input {...useFormField('firstName', '', validateName)} />
      {useFormError('firstName')}
    </div>
  </div>
));

const FamilyName: React.FC = React.memo(() => (
  <div>
    {Math.random()}
    <div>
      Family Name:
      <input {...useFormField('familyName', '', validateName)} />
      {useFormError('familyName')}
    </div>
  </div>
));

const Gender: React.FC = React.memo(() => (
  <div>
    {Math.random()}
    <div>
      Gender:
      <select {...useFormField('gender', 'na')}>
        <option value="na">N/A</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
      </select>
      {useFormError('gender')}
    </div>
  </div>
));

const Teenager: React.FC = React.memo(() => (
  <div>
    {Math.random()}
    <div>
      Teenager:
      <input type="checkbox" {...useFormField('teenager', false)} />
      {useFormError('teenager')}
    </div>
  </div>
));

const PersonForm: React.FC = React.memo(() => {
  const formValues = useFormValues();
  const onSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    // eslint-disable-next-line no-console
    console.log(formValues);
  };
  return (
    <form onSubmit={onSubmit}>
      {Math.random()}
      <button type="submit" disabled={formValues === null}>Submit</button> {/*This button will rerender every typing*/}
      <h3>First Name</h3>
      <FirstName />
      <h3>Family Name</h3>
      <FamilyName />
      <h3>Gender</h3>
      <Gender />
      <h3>Teenager</h3>
      <Teenager />
    </form>
  );
});

const FormHolder: React.FC = () => (
  <FormProvider>
    <PersonForm />
  </FormProvider>
);

export default FormHolder;

Is it different from MobX?

I just found a comment by @FredyC in facebook/react#15156 (comment).

@fuleinist Ultimately, it's not that different from MobX, although a lot simplified for a specific use case. MobX already works like that (also using Proxies), the state is mutated and components who use specific bits of the state get re-rendered, nothing else.

I'm not very familiar with MobX, so please correct me if I'm wrong.
As far as I understand, MobX uses Proxies to trap object mutations, not object property access in render function. I mean, what it's providing is not an alternative to Redux/ReactHooks reducers.
Theoretically, MobX is complementary to react-tracked. I'm not yet sure if/how it can be implemented, though.
react-tracked doesn't change the semantics of render function. It tracks the state usage and trigger re-renders. How to update state is not the focus of the library (at least at the moment).

I wrote an example to show the power of state usage tracking in the "Advanced Example" in https://blog.axlight.com/posts/effortless-render-optimization-with-state-usage-tracking-with-react-hooks/.


(edit) There was a misunderstanding of mine. useObserver tracks the state usage.

Usage with Jest/JSDOM

Seems Proxies are not yet supported in JSDOM, hence writing unit tests with react-tracked and Jest is throwing errors.

Are there any ideas on how this can be mocked in the UT environment ?

Suggestion to add `useSelectors`

Hey @dai-shi, love this package and the work you're doing here. This solves the context re-rendering problem really elegantly.

Just a suggestion if we should add an additional API method called useSelectors. It takes an array of selectors as input and outputs an array of selected states that can be accessed via destructuring:

const [a, b, c] = useSelectors(getA, getB, getC);  // it can call `useSelector` under the hood.

Currently, to do this, we would use multiple lines of useSelector. Obviously, users can implement useSelectors themselves but just wanted to check if it's worthwhile to include in this library!

How Redux Style Guide can be applied to React Tracked with ReactHooks useReducer.

Redux Style Guide: https://redux.js.org/style-guide/style-guide

  • Do Not Mutate State: Yes
  • Reducers Must Not Have Side Effects: Yes
  • Do Not Put Non-Serializable Values in State or Actions: Yes, but not necessarily
  • Only One Redux Store Per App: Yes but not necessarily
  • Use Redux Toolkit for Writing Redux Logic: No
  • Use Immer for Writing Immutable Updates: Yes, but not necessarily
  • Structure Files as Feature Folders or Ducks: Maybe
  • Put as Much Logic as Possible in Reducers: Yes
  • Reducers Should Own the State Shape: Maybe
  • Name State Slices Based On the Stored Data: Maybe
  • Treat Reducers as State Machines: Yes
  • Normalize Complex Nested/Relational State: Yes, but not so recommended
  • Model Actions as Events, Not Setters: Yes
  • Write Meaningful Action Names: Yes
  • Allow Many Reducers to Respond to the Same Action: Yes
  • Avoid Dispatching Many Actions Sequentially: Yes
  • Evaluate Where Each Piece of State Should Live: Yes
  • Connect More Components to Read Data from the Store: Yes, but not so recommended
  • Use the Object Shorthand Form of mapDispatch with connect: N/A
  • Call useSelector Multiple Times in Function Components: Yes if using useSelector
  • Use Static Typing: Yes
  • Use the Redux DevTools Extension for Debugging: No, use React Dev Tools
  • Write Action Types as domain/eventName: Yes
  • Write Actions Using the Flux Standard Action Convention: Yes but not necessarily
  • Use Action Creators: Yes but not necessarily
  • Use Thunks for Async Logic: No, but use similar libs
  • Move Complex Logic Outside Components: No, use hooks
  • Use Selector Functions to Read from Store State: Yes, but useTrackedState is preferable
  • Avoid Putting Form State In Redux: Yes, but not impossible

Originally posted: https://twitter.com/dai_shi/status/1217104754582536192

Discussion: how to plugin a new feature like middleware

react-tracked doesn't have a plugin system like reactive-react-redux which can use redux middleware system.

We shouldn't reinvent redux middleware like system. react-tracked should stick with react contexts.
React hooks oriented solution would be something like function composition instead of middleware system.

A naive idea: As we have createContainer, can we utilize it?

// pattern1
const container = extendContainer(createContainer(useValue));

// pattern2
const container = enhance(createContainer)(useValue);

Question: did you benchmark it?

Hello! the idea behind react-tracked seems interesting.

You claim the performances are better but did you actually verify that? Aren't you creating a completely new proxy on each update?

Cheers

Discussion: Many providers in application.

Hi !! :-) Rafael here again. lol

I have a provider as below:

import React, { useReducer } from "react";
import { createContainer } from "react-tracked";

const reducer = (state, newObject) => { return { ...state, ...newObject } };

export const { Provider, useTracked: useContainerState } = createContainer(
    () => useReducer(reducer, {})
);

export const ContainerProvider = ({ children }) => <Provider> {children} </Provider> ;

And I'm using that provider wrapping any route of my application. To create a context to any component route.

const PrivateRoute = ({ component: Component, ...rest }) => {
    return (
        <Route
            {...rest}
            render={props => validLogin()
                ? <ContainerProvider> <Component {...props} {...rest} /> </ContainerProvider>
                : <Redirect to="/login"/>
            }
        />
    );
};

const WorkflowRoutes = () => (
    <Switch>
        <PrivateRoute exact path="/dashboard" component={Dashboard} />
        <PrivateRoute exact path="/audit" component={Audit} />
    ....

The problem is...
If I access the dashboard module and then access the audit module, the object contained in the state (useTracked) in audit module will have all the objects that were in the dashboard. It is sharing the data instead of having one provider for each module.

Is there any way to create a unique provider for each module?
I imagine this is happening because the reducer is not being created in ContainerProvider.
Is there any way around this?

Thanks a lot again !

Rafael.

How can I return multiple useState or functions with custom hook?

Hi @dai-shi , I'm very interested about your library I also used to use react-global-state-hook. Now I move to React-tracked and I have a question about it I want to return count1 and setCountOrig1, delPosts also. What can I do? thanks you

//custom hook  I used to do like this before:
return {
   delPosts ,
    count, 
    setCoun,
    count1,
    setCountOrig1 
  };

Example :

import { createContainer } from 'react-tracked';
const useValue = () => {
  const [count, setCountOrig] = useState(0);
  const [count1, setCountOrig1] = useState(0);

  const setCount = (nextCount: number) => {
    // eslint-disable-next-line no-console
    console.log({ nextCount });
    setCountOrig(nextCount);
  };

  //delete posts
  const delPosts = useCallback(
    async id => {
      const newData = filter(dataPosts?.data, o => o.id !== id);
      mutatePosts({ ...dataPosts, data: [...newData] });
      await api.delPosts(id);
      //refetch
      refreshPosts();
    },
    [dataPosts, mutatePosts, refreshPosts]
  );
  return [count, setCount] as const;

const { Provider, useTracked } = createContainer(useValue);

Problems of updating my component after updating the context

I used useReducer, and the hierarchy of context(trackedState) is deepermy. When I updated one of the values,for example

const changeType = (state: SchemaState, keyRoute: KeyRoute, fieldType: SchemaType): SchemaState => {
  // set field new value
  set(state, keyRoute, defaultSchema[fieldType]);

  return state;
};

My component used this value don't update.

So, I force return a value after deep copy

const changeType = (state: SchemaState, keyRoute: KeyRoute, fieldType: SchemaType): SchemaState => {
  const schema = cloneDeep(state);

  // set field new value
  set(schema, keyRoute, defaultSchema[fieldType]);

  return schema;
};

But, no i face a new problem,some of the elements on my page have onblur event, some have onclick events. Sometimes they will trigger at the same time.

Because of deep copying, only one of the two dispatches can take effect

Can you give me some good suggestions?

Memo doesn't work with forwarded ref

Hello and thank you for all this work! 🤗

When using the memo function (from react-tracked) with a component using React.forwardRef, I encounter several errors:

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

By changing the code in this way, everything works again ⬇️

export function memo(
  Component: any,
  propsAreEqual?: any,
) {
  const WrappedComponent = forwardRef((props: any, ref: any) => {
    Object.values(props).forEach(trackMemo);
    return createElement(Component, { ...props, ref );
  });
  return reactMemo(WrappedComponent, propsAreEqual);
}

Have you ever encountered this kind of error? I can open a PR if you want!

Greetings from Paris 🇫🇷.

Select() function does not work with use-saga-reducer

The problem:
Calling yield select() inside a saga always returns the initial state, and not the actual current state. I need to access the current state inside a saga function, is there a different way to do this or is this a bug that needs fixing?

Steps to reproduce:
In examples/13_saga/src/store.ts, import select from redux-saga/effects:

import {
  call,
  put,
  delay,
  takeLatest,
  takeEvery,
  all,
  select
} from 'redux-saga/effects';

And change delayedDecrementer() to be:

function* delayedDecrementer() {
  const count = yield select(state => state.count);
  console.log(`The count is: ${count}`)
  yield delay(500);
  yield put({ type: 'DECREMENT' });
}

Now, open the app in the browser, click the plus button a few times so the count equals 3 (or any number > 0). Then click "-1 Delayed" button.

What actually happens:
The console logs display the message The count is: 0.

What should happen:
The console logs should show the actual count value before the decrement i.e. The count is: 3.

[v0.8] Typescript error in Provider when passing props to createContainer

I want to say I really really LOVE the changes in v0.8!

I'm running into a Typescript error when trying to use Provider with props that are passed into createContainer.

Here is a snippet of my code:

// state.tsx (no typescript errors)
import * as types from 'types';

export const {
  Provider,
  useSelector,
  useUpdate: useDispatch,
} = createContainer(({ initialState, onChange }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  // this was really cool that I can wrap useReducer dispatch with whatever custom logic I 
  // want here and return a wrappedDispatch.  I see that you have a PR about ideas on 
  // implementing middlewares in react-racked, not sure if this pattern will give some ideas.
  function update(action: types.Action): void {
    dispatch(action);
    if (onChange && !blackListedActions.includes(action.type)) {
      onChange(state.filters, action);
    }
  }
  return [state, update];
});


// component.tsx (typescript error on Provider)
import { Provider} from 'state';

<Provider
  initialState={getInitialState(filters)}
  onChange={onChange}>
  <Search isDisabled={isDisabled} isLoading={isLoading} />
</Provider>

Over here, I see a Typescript error on Provider that reads:

src/index.tsx:45:14 - error TS2322: Type '{ children: Element; initialState: State; onChange: ChangeHandler; }' is not assignable to type 'IntrinsicAttributes & { children?: ReactNode; }'.
Property 'initialState' does not exist on type 'IntrinsicAttributes & { children?: ReactNode; }'.
45             <Provider

It doesn't know anything about the props (initialState, onChange) that are requested when creating createContainer. Is there something I have to do explicitly when creating the container to have this not complain?

How I'm organizing my state code with react-tracked.

I'm a huge fan of this library, and really like the simplified approach and API. Wanted to give some ideas how I'm using this to organize my project so far!

I like how I end my state files with exporting react-tracked methods, so anything state-related (actions, reducers, tracked-state, selectors) can all be consumed/imported from one file. I can see this scale really nicely with more folders/files e.g.

/state
  /user
    index.ts  # exports reducer, actions, selectors and react-tracked APIs
    reducer.ts  # can be a single DUCKs-like file, but I re-export them to index.ts anyway.
    actions.ts
    selectors.ts
  /cart
    index.ts
    reducer.ts
    actions.ts
    selectors.ts
  /etc
    index.ts
    reducer.ts
    actions.ts
    selectors.ts

Excited to see where this goes, and I believe if there is integrations with middleware and dev-tooling, it will pick up rapidly. I had some code with wrapping dispatches, but I don't know enough to see how to organize it as middlewares for react-tracked. Happy to brainstorm together! I think there MIGHT be ways to hack and merge the APIs into redux-dev-tools and piggy back on the existing redux-dev-tools to render state changes.

Typescript is not happy with react-tracked. What am I missing here?

Ideas how to work around this with Provider?

(alias) const Provider: React.ComponentType
import Provider
JSX element type 'ReactElement<any, any> | Component<unknown, any, any>' is not a constructor function for JSX elements.
Type 'Component<unknown, any, any>' is not assignable to type 'Element | ElementClass'.
Type 'Component<unknown, any, any>' is not assignable to type 'ElementClass'.
The types returned by 'render()' are incompatible between these types.
Type 'ReactNode' is not assignable to type 'false | Element'.
Type 'string' is not assignable to type 'false | Element'.ts

RFC: new API design with breaking changes

There's some drawbacks in the current design and I would like to redesign the API.
This is a good moment to get feedback because this is going to be a breaking change.

Problem 1:

<Provider useValue={...}> takes a hook as a prop. This is not very good because props may change. Currently, to mitigate the problem, it warns if useValue is changed between renders.

My proposal for problem 1:

We only accept useReduder style state.
<Provider reducer={...} initialState={...} init={...}>

Problem 2:

createContainer(() => useReducer(...)) doesn't allow receiving initial value from props.

My proposal for problem 2:

Make it like createContainer((props) => ...), and accept props from <Provider {...props}>


I would like to hear +1, -1, comments, suggestions, other problems, and so on. Thanks for your contribution!

Dispatch function not getting called

I have tried the Todo useReducer example and did some modifications.

I have created a wrapper of input and throwing onChange callback to TodoList which will call dispatch. As per the expected behaviour, the dispatch function should be called and it should update the filter. But, the dispatch function is not working as expected and the global state is not updating. I'm thinking that it is getting called but for some reason it is not calling the reducer function.

Please check the code @ https://codesandbox.io/s/react-tracked-issue-i5dnq

I have noticed that it is working as expected with HTML's input and material UIs Button. But not working If I create a wrapper or with material UIs MUISelect ( checked with only these so far :) )

Discussion: How do I change and read an object within another object?

I have the following object:

{
	"data": [],
	"others": {},
	"person": {
		"name" : "John",
		"age": 29,
		"address": {
			"street": "street 1",
			"number": 39,
			"zip": 43949,
			"country": "eua",
			"city": "test"
		},
	}
}

And this simple Store:

import React, { useReducer } from "react";
import { createContainer } from "react-tracked";

const reducer = (state, newObject) => {
    return {...state,...newObject};
};

export const { Provider, useTracked, useTrackedState } = createContainer( () => useReducer(reducer, {}) );

export const ContainerProvider = ({ children }) => <Provider> {children} </Provider> ;

How I use the Store:

<ContainerProvider> <PersonForm /> </ContainerProvider>

My PersonForm:

const [containerState, containerDispatch] = useTracked();

useEffect(() => {
    console.log(containerState.person);
}, [containerState]);

<InputText
    value={containerState.person.name || ""}
    onChange={e => containerDispatch({'name': {...containerState.person, "name": e.target.value}})}
/>
<InputText
    value={containerState.person.age || ""}
    onChange={e => containerDispatch({'age': {...containerState.person, "age": e.target.value}})}
/>

When I change the "name" property, I lost another inputs values (age, address, etc) and I see this on console:
Proxy {name: "test"}

When I change the "age" property, I lost another inputs values (name, address, etc) and see this on console:
Proxy {age: 19}

Questions:

1) How do I get access to the entire state ?
2) How do I change the data keeping the previous data ?

Thank you so much.

Calling async actions from other async actions

Hey, as in the subject. Is it possible to call async action from within other async action?

In Redux + Redux Thunk, it's possible to do something like:

export const asyncAction = (args: Args): ThunkResult<void> => async (
  dispatch,
  getState
) => {
  try {
    await dispatch(anotherAsyncAction());
  } catch (error) {
    /* ... */
  }
};

But it doesn't seem to be possible in ReactTracked. Is it?

Keeping React-Tracked Fast

I have been using this library for over a year, I have a simple question that I would like to know about.

Is it alright to store large objects/(arrays of complex objects) in the state and does it effect the performance of dispatch? Let's say that I have the following state:

const defaultState = {
userLoggedIn:false, 
userPosts:[] // Array of post objects
}

If I am only updating userLoggedIn value in dispatch, will the dispatch function take more time to run if the size of userPosts is large?

Update state from event listener in useEffect

Am I allowed to update state from event listener ?

    useEffect(() => {
        window.addEventListener("keydown", () => {
             updateState("something);
         });
         return () => window.removeEventListener("keydown", listener);
    }, [])

I'm asking this question, because it seems that doing it this way, will trigger full update on all hooks created with useSelector.

ps. Please let me know if full blown codesandbox example is needed

Several TypeScript type problems

Hey, thanks again for the great package! I'm trying to switch my project from redux to react-tracked and overall everything works but I've encountered several typing problems that are a little bit annoying but I'm not big TS expert to find solution for them. However, I've prepared reproduction repository and I'm going to describe all the cases.

1. The useValue return type not compatible when using useReducerAsync with destructuring.

Let's consider such code example:

const useValue = () => {
  const [state, dispatch] = useReducerAsync<
    Reducer<State, Action>,
    AsyncAction,
    AsyncAction
  >(reducer, initialState, asyncActionHandlers);
  /* some code goes here... */
  return [state, dispatch];
};

Now if I try to use it:

export const {
  Provider: StateProvider,
  useUpdate: useDispatch,
  useSelector
} = createContainer(useValue);

I will get type error for the useValue:

const useValue: () => (State | Dispatch<{
    type: "SET_VALUE";
    value: number;
} | {
    type: "SET_LOADING";
    loading: boolean;
} | AsyncActionLoadValueWait | AsyncActionLoadValue>)[]
Argument of type '() => (State | Dispatch<{ type: "SET_VALUE"; value: number; } | { type: "SET_LOADING"; loading: boolean; } | AsyncActionLoadValueWait | AsyncActionLoadValue>)[]' is not assignable to parameter of type '(props?: unknown) => [State | Dispatch<{ type: "SET_VALUE"; value: number; } | { type: "SET_LOADING"; loading: boolean; } | AsyncActionLoadValueWait | AsyncActionLoadValue>, State | Dispatch<...>]'.
  Type '(State | Dispatch<{ type: "SET_VALUE"; value: number; } | { type: "SET_LOADING"; loading: boolean; } | AsyncActionLoadValueWait | AsyncActionLoadValue>)[]' is missing the following properties from type '[State | Dispatch<{ type: "SET_VALUE"; value: number; } | { type: "SET_LOADING"; loading: boolean; } | AsyncActionLoadValueWait | AsyncActionLoadValue>, State | Dispatch<{ type: "SET_VALUE"; value: number; } | { ...; } | AsyncActionLoadValueWait | AsyncActionLoadValue>]': 0, 1ts(2345)

To solve that, I'm just doing:

const useValue = () => {
  const [state, dispatch] = useReducerAsync<
    Reducer<State, Action>,
    AsyncAction,
    AsyncAction
  >(reducer, initialState, asyncActionHandlers);
  return [state, dispatch] as [State, Dispatch<AsyncAction>];
};

So it looks like some TS error and nothing related to your library but I might be wrong. Any ideas?

2. If I use useReducerAsync in the useValue then useUpdate only expects async actions

I'm using async actions according to the examples in docs and when I do:

import { useCallback } from "react";
import { useDispatch } from "./store";

export const Button: React.FC = () => {
  const dispatch = useDispatch(); // useDispatch is an alias for useUpdate.

  const handleClick = useCallback(() => {
    dispatch({ type: "LOAD_VALUE_WAIT", wait: 200 });
  }, [dispatch]);

  return <button onClick={handleClick}>Load</button>;
};

then the dispatch method only expects me to invoke it with AsyncAction:

const dispatch: (value: AsyncAction) => void

3. Add more info about types and why they are being used

I don't know what's the point of having OuterAction type. Is there any reason beside distinguishing between actions being called from async actions?

If I do:

useReducerAsync<
    Reducer<State, Action>,
    AsyncAction,
    AsyncAction | OuterAction
  >(reducer, initialState, asyncActionHandlers);

then I can only use OuterActions in the async actions so I had to change type from AsyncAction | OuterAction to AsyncAction | Action. Overall, the types used here are not described anywhere and it's hard to tell where should go what to properly type project.

[v2] Testing fails on second render

Here's a fairly full example which fails for me:

import { cleanup, fireEvent, render } from '@testing-library/react'
import React, { StrictMode, useLayoutEffect, useReducer } from 'react'
import { createContainer } from 'react-tracked'

describe('basic tests', () => {
  afterEach(cleanup)

  it('verifies react-tracked supports tracking before insert', () => {
    const useValue = ({ reducer, initialState }) =>
      useReducer(reducer, initialState)
    const { Provider, useTracked } = createContainer<any, any, any>(useValue)

    const initialState = {}
    const App = () => {
      return (
        <Provider reducer={reducer} initialState={initialState}>
          <Component />
        </Provider>
      )
    }

    const reducer = (state: any, action: any) => {
      return {
        ...state,
        ...action.next,
      }
    }

    const Component = () => {
      const [store, dispatch] = useTracked()

      useLayoutEffect(() => {
        dispatch({ next: { x: 1 } })
      }, [])

      return (
        <>
          <div title="test">{store.x ?? 'none'}</div>
          <button
            title="add"
            onClick={() => {
              dispatch({ next: { x: 2 } })
            }}
          />
        </>
      )
    }

    const t1 = render(<App />)
    expect(t1.getAllByTitle('test')[0].innerHTML).toBe('1')
    fireEvent.click(t1.getAllByTitle('add')[0])
    expect(t1.getAllByTitle('test')[0].innerHTML).toBe('2')

    const t2 = render(<App />)
    expect(t2.getAllByTitle('test')[0].innerHTML).toBe('1')
    fireEvent.click(t2.getAllByTitle('add')[0])
    expect(t2.getAllByTitle('test')[0].innerHTML).toBe('2')
  })
})

The t1 tests run fine, but t2 for whatever reason gets the old value. Thought this may be a bug with @testing-library/react but when I tried switching react-test-renderer I got the same thing when rendering again. Perhaps I'm using it wrong here, not sure, but the same test was working before moving to experimental react, so guessing this may be an experimental React thing.

What is trackMemo and when to use it

What is state usage tracking

Let's say we have a state like this.

const state = { a: { b: 1, c: 2 } };

We then use this state in render:

return (
  <div>{state.a.b}</div>
);

With state usage tracking, it will mark .a.b is used.
So, only when state.a.b is changed, it triggers re-render.
If only state.a.c is change, it won't.

What would be a problem

If the render function is really pure, that is, it is consistent with using state in every render,
there's no problem.
However, suppose the function uses state.a.b for the first time,
but does not use it next time, then it will forget the usage marked in the first time.
If that's intentional, it fine. But, this could happen with memoization unexpectedly.

For example:

const b = useMemo(state => state.a.b, [state.a]);
const x = state.a.c + 1;

This might cause a problem.

  1. state = { a: { b: 1, c: 2 } }
  2. usage = ['.a.b', '.a.c']
  3. state = { a: { b: 1, c: 3 } }
  4. usage = ['.a.c'] // not ['.a', '.a.c'], because we only care leaves.
  5. state = { a: { b: 4, c: 3 } }
  6. no re-render

When we should use trackMemo

If a render function uses some kind of memoization based on object identity.
We should mark the whole object as used. This is what trackMemo is for.

Its implementation seems trivial, but this is only required if render is not pure for using state.
Only such case known so far is memoization. So, it's named trackMemo.

How to work with arrays?

Hi,

I just found react-tracked and was really intrigued by the simple api. But I'm having trouble getting it to work to just re-render the state that has changed. I tried to follow the quick-start, but I'm using an array instead of an object as a state.

When I update an item in the array in an immutable way (recreating the whole array) then all items still rerender. It also slowed down the whole application and made firefox consume a lot of cpu.

I guess that my approach is completely wrong and could even be related to my current understanding of javascript/react.

I created a codesanbox that illustrates the problem. It shows the date and time in seconds every time the todo-component is rerendered.

If you have the time it would be interesting to get your feedback on what I'm supposedly are doing wrong here :)

Thanks,
Niklas

Performance/differences compared with redux

I can provide an example if need be, but I've been looking at using your library rather than redux for a component I'm building. One major difference I notice is that, whenever any state changes, every single component in the tree does run the functional components (can see this by adding console logs within them, for all of your examples)

My current implementation using redux, and useSelector, only actually logs the components that have changed based on state. e.g. the textbox being updated, whereas switching to this, I actually see every single print a console log out, even if it is not impacted.

So is this a key difference? Would you expect the logic within components to still be ran? even if the rendering is optimised (Which i'd expect react to take care of anyway?)

A new API for reverting proxy

A workaround for the current third caveat is missing.

Proxied state shouldn't be used outside of render

We could copy an object as a workaround, but it's extra cost.

It would be nice to have a new API to get an original object from a tracked object.

Usage without <Provider />

Would it be possible to use this without provider, at least as an option? Would help me with an abstraction I'm attempting to build.

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.