Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

createSharedState #130

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
124 changes: 124 additions & 0 deletions text/0000-create-shared-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
- Start Date: 2019-11-08
- RFC PR: (leave this empty)
- React Issue: (leave this empty)

# Summary

I would like to add a single function which allows sharing data between components with hooks. This could be used instead of Context API when scoping or cascading to single component structure branch is not needed. In many cases this leads to more simple implementation compared to using Context API. `createSharedState` creates a hook very similar to `useState` but with sync state across hook instances. Setting a value in one component will result re-rendering every component which uses the same hook. The state is preserved even if all hooks become unmounted.

![image](https://user-images.githubusercontent.com/3163392/68551190-7fe32c80-040a-11ea-935c-e390f1121a24.png)

## Side-by-side comparison with Context API
![image](https://user-images.githubusercontent.com/3163392/68534701-aedc9e00-0337-11ea-89c3-7eed540f23cd.png)

# Basic example

I suggest having an API such as this:

```jsx
import { createSharedState } from 'react';

const useTheme = createSharedState('light');

function App {
return (
<Toolbar />
<ThemeSwitch />
);
}

function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
const [theme] = useTheme();

return <Button theme={theme} />;
}

function ThemeSwitch {
const [theme, setTheme] = useTheme();

return (
<Button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme}
</Button>
);
}
```

For full working example please check https://codesandbox.io/s/react-create-shared-state-demo-9s9ui.

# Motivation

`createSharedState` results with a cleaner codebase with easily reusable shared state. Using this function will eliminated not necessary re-renders caused by top-level `Provider` in case of syncing data between nested components on different branches of component tree.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand your statement correctly, I think it is inaccurate. Just because a higher-level component re-renders does not mean that all its children will necessarily re-render. https://kentcdodds.com/blog/optimize-react-re-renders

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can optimize for sure but without any manual intervention everything below provider will be rerendered.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mucsi96, wheh a Provider changes value, children are not re-rendered, unless they are subscribed to the same context.
Demo: https://codesandbox.io/s/pedantic-moore-cxfq5


In React community many projects are already created with very similar idea:
- https:/pie6k/hooksy
- https:/kelp404/react-hooks-shared-state
- https:/philippguertler/react-hook-shared-state
- https:/dimapaloskin/react-shared-hooks
- https:/atlassian/react-sweet-state
- https:/paul-phan/global-state-hook
- https:/donavon/use-persisted-state

Even though the solution can be easily created in user-land my motivation is the following to add it to core API:
- Most of developers would not know about it's existence if it's not mentioned in React documentation. Usually training sessions, blogs post about React are based on that React core API documentation and tutorial
- The code is very small and simple. It will not increase the bundle size much

# Detailed design

The design could be very simple. This is naive implementation based on public React API.

```jsx
import { useState, useEffect } from 'react';

export function createSharedState(defaultValue) {
const listeners = new Set();
let backupValue = defaultValue;

return () => {
const [value, setValue] = useState(backupValue);

useEffect(() => {
listeners.add(setValue);
return () => listeners.delete(setValue);
}, []);

useEffect(() => {
backupValue = value;
listeners.forEach(listener => listener !== setValue && listener(value));
}, [value]);

return [value, setValue];
};
}
Comment on lines +83 to +102

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done this in an internal project at work, and I highlight recommend doing these changes:

  • Keep the setter separate, it allows you use to use it:
    • for setup, outside components.
    • in class components.
    • in function component without "hooking" onto the state, so if you only use the setter, it won't trigger a rerender for that component on state changes.
    • in tests, to reset the state for example.
  • Update the state immediately in the useEffect, as it might have been updated after the render, and before the effect call.
Suggested change
export function createSharedState(defaultValue) {
const listeners = new Set();
let backupValue = defaultValue;
return () => {
const [value, setValue] = useState(backupValue);
useEffect(() => {
listeners.add(setValue);
return () => listeners.delete(setValue);
}, []);
useEffect(() => {
backupValue = value;
listeners.forEach(listener => listener !== setValue && listener(value));
}, [value]);
return [value, setValue];
};
}
export function createSharedState(initialState) {
const listeners = new Set();
let sharedState = initialState;
function useSharedState() {
const [state, setState] = useState(sharedState);
useEffect(() => {
listeners.add(setState);
setState(sharedState);
return () => {
listeners.delete(setState);
};
}, []);
return state;
}
function setSharedState(newState) {
if (typeof newState === 'function') {
newState = newState(sharedState);
}
if (!Object.is(sharedState, newState)) {
sharedState = newState;
for (const listener of listeners) {
listener(sharedState);
}
}
}
return [useSharedState, setSharedState];
}

```

For more details please check the [`react-create-shared-state`](https:/mucsi96/react-create-shared-state) NPM package.

# Drawbacks

As with any React hooks this works only in functional components.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would create a singleton which has the drawback of not working very well in SSR environments. I think that's a ship stopper personally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect in SSR environment the initial value to be rendered. Can you explain please what is the issue with SSR environments?


# Adoption strategy

This is not a breaking change just addition to API.
Migration from Context API to `createSharedState` can be done gradually in application.
The concept itself can be tested with [`react-create-shared-state`](https:/mucsi96/react-create-shared-state) NPM package.

# How we teach this

React API documentation need to be extended with documentation for `createSharedState` function.
Also I recommend it's adoption in the tutorial and mentioning in Context API page.

# Unresolved questions

- How can we implement this with internal React API?