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

Shareable container implementation #198

Merged
merged 4 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {
// fix for eslint-plugin-flowtype/384 not supporting wildcard
_: 'readonly',
},
plugins: ['react', 'react-hooks', 'import', 'flowtype'],
plugins: ['react', 'react-hooks', 'import'],
rules: {
'no-shadow': ['error'],
indent: ['off'],
Expand Down
2 changes: 1 addition & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
- [Selectors](/advanced/selector.md)
- [Containers](/advanced/container.md)
- [Devtools](/advanced/devtools.md)
- [State rehydration](/advanced/rehydration.md)
- [Middlewares](/advanced/middlewares.md)
- [Custom mutator](/advanced/mutator.md)

Expand All @@ -34,6 +33,7 @@

* **Recipes**

- [State rehydration](/recipes/rehydration.md)
- [Flow types](/recipes/flow.md)
- [Typescript types](/recipes/typescript.md)
- [Composition](/recipes/composition.md)
1 change: 0 additions & 1 deletion docs/advanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
- [Selectors](./selector.md)
- [Containers](./container.md)
- [Devtools](./devtools.md)
- [State rehydration](./rehydration.md)
- [Middlewares](./middlewares.md)
- [Custom mutator](./mutator.md)
49 changes: 33 additions & 16 deletions docs/advanced/container.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
## Containers

While sweet-state promotes independent, globally accessible micro-Stores, such behaviour only allows one instance for each Store type. Sometimes though you might want to have multiple instances of the same Store, and that's where `Container` components come into play. They allow you to create independent Store instances of the same type, making them either globally available (app-wide) or just locally (so only accessible to children hooks/subscribers).
> _Note: we recently improved Containers API to be more flexibile and safe. These docs now use the new Store's `containedBy` and `handlers` attributes. You still can find the old one documented [here](../api/container.md)._

While sweet-state promotes independent, globally accessible micro-Stores, such behaviour only allows one instance for each Store type and might become problematic as your app grows. As an example, you might want to have multiple instances of the same Store, or some store data to be cleaned up once a component unmounts. That's where `Container` components come into play. They allow you to create independent Store instances of the same type, making them either globally available (app-wide) or just locally (so only accessible to children hooks/subscribers).

```js
// components/counter.js
import { createStore, createContainer, createHook } from 'react-sweet-state';

export const CounterContainer = createContainer();

const Store = createStore({
containedBy: CounterContainer,
initialState: { count: 0 },
actions: {
increment:
Expand All @@ -18,7 +23,6 @@ const Store = createStore({
},
});

export const CounterContainer = createContainer(Store);
const useCounter = createHook(Store);

export const CounterButton = () => {
Expand Down Expand Up @@ -54,7 +58,7 @@ const App = () => (
);
```

The power of `Container` is that you can expand or reduce the scope at will, without requiring any change on the children. That means you can start local and later, if you need to access the same state elsewhere, you can either move the `Container` up in the tree, add the `scope` prop to "link" two separate trees or remove the container altogether.
The power of `Container` is that you can expand or reduce the scope at will, without requiring any change on the children. That means you can start local and later, if you need to access the same state elsewhere, you can either move the `Container` up in the tree or add the `scope` prop to "link" two separate trees or add `isGlobal` and make it singleton once again.

### Additional features

Expand All @@ -80,10 +84,11 @@ const App = () => (

#### Container props are available in actions

Props provided to containers are passed to Store actions as a second parameter [see actions API](../api/actions.md). This makes it extremely easy to pass dynamic configuration options to actions.
Props provided to containers are passed to Store `actions` and `handlers` as a second parameter [see actions API](../api/actions.md). This makes it extremely easy to pass dynamic configuration options to actions.

```js
const Store = createStore({
containedBy: CounterContainer,
initialState: { count: 0 },
actions: {
increment:
Expand All @@ -105,23 +110,35 @@ const App = () => (
);
```

_NOTE: Remember though that those props will **only** be available to hooks/subscribers that are children of the `Container` that receives them, regardless of the Container being global/scoped._
> _NOTE: Remember though that those props will **only** be available to hooks/subscribers that are children of the `Container` that receives them, regardless of the Container being global/scoped._

#### Container can trigger actions
#### Container enables additional Store handlers

`Container` options have `onInit` and `onUpdate` keys, to trigger actions and update the state on its props change. The methods' shape is the same as all other actions.
By providing `containedBy` to a store you can enable additional `handlers` that trigger functions when specific events occur. Current supported handlers are: `onInit`, `onUpdate`, `onDestroy` and `onContainerUpdate`, and these methods' shape is the same as all other actions.

```js
const CounterContainer = createContainer(Store, {
onInit:
() =>
({ setState }, { initialCount }) => {
setState({ count: initialCount });
},
const Store = createStore({
containedBy: CounterContainer,
initialState: { count: 0 },
actions: {
increment:
() =>
({ setState }, { multiplier }) => {
const currentCount = getState().count * multiplier;
setState({ count: currentCount + 1 });
},
},
handlers: {
onContainerUpdate:
() =>
({ setState }, { multiplier }) => {
setState({ count: 0 }); // reset state on multiplier change
},
},
});

const App = () => (
<CounterContainer scope={'counter-1'} initialCount={10}>
const App = ({ n }) => (
<CounterContainer scope={'counter-1'} multiplier={n}>
{/* this starts from 10 */}
<CounterButton />
</CounterContainer>
Expand All @@ -130,7 +147,7 @@ const App = () => (

#### Scoped data cleanup

Store instances created by `Container`s without `isGlobal` are automatically cleared once the last Container accessing that Store is unmounted.
Store instances created by `Container`s without `isGlobal` are automatically cleared once the last Container accessing that Store is unmounted. At that point `handlers.onDestroy` will also be called.

---

Expand Down
27 changes: 0 additions & 27 deletions docs/advanced/rehydration.md

This file was deleted.

85 changes: 65 additions & 20 deletions docs/api/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@
### createContainer

```js
createContainer(Store, [options]);
createContainer([options]);
```

##### Arguments
This is the recommended way of creating containers, passing them as `containedBy` attribute on store creation.

1. `Store` _(Object)_: The store type returned from a call to `createStore`
##### Arguments

2. [`options`] _(Object)_: containing one or more of the following keys:
1. [`options`] _(Object)_: containing one or more of the following keys:

- `displayName` _(string)_: Used by React to better identify a component. Defaults to `Container(${storeName})`

- `onInit` _(Function)_: an action that will be triggered on container initialisation. If you define multiple containers this action will be run each time one of the container components is initialised by React.

- `onUpdate` _(Function)_: an action that will be triggered when props on a container change.

- `onCleanup` _(Function)_: an action that will be triggered after the container has been unmounted. Useful in case you want to clean up side effects like event listeners or timers, or restore the store state to its initial state. As with `onInit`, if you define multiple containers this action will trigger after unmount of each one.

##### Returns

_(Component)_: this React component allows you to change the behaviour of child components by providing different Store instances or custom props to actions. It accept the following props:
Expand All @@ -32,23 +26,74 @@ _(Component)_: this React component allows you to change the behaviour of child

##### Example

Let's create a Container that automatically populates the todos' Store instance with some todos coming from SSR, for instance.
Let's create a Container that initializes all theme-related stores:

```js
// theming.js
import { createContainer } from 'react-sweet-state';
import Store from './store';
export const ThemeContainer = createContainer();

// colors.js
import { ThemeContainer } from './theming';
const ColorsStore = createStore({
// ...
containedBy: ThemeContainer,
});
// We can also have a FontSizesStore that has the same `containedBy` value

// app.js
const UserTheme = ({ colors, sizes }) => (
<ThemeContainer initialTheme={{ colors, sizes }}>
<TodosList />
</ThemeContainer>
);
```

### createContainer as override

> _Note: this API configuration provides less flexibility and safety nets than using Store's `containedBy` and `handlers`, so we recommend using this style mostly for testing/storybook purposes._

```js
createContainer(Store, [options]);
```

const TodosContainer = createContainer(Store, {
##### Arguments

1. `Store` _(Object)_: The store type returned from a call to `createStore`

2. [`options`] _(Object)_: containing one or more of the following keys:

- `displayName` _(string)_: Used by React to better identify a component. Defaults to `Container(${storeName})`.

- `onInit` _(Function)_: an action that will be triggered on store initialisation. It overrides store's `handlers.onInit`.

- `onUpdate` _(Function)_: an action that will be triggered when props on a container change. It is different from store's `onUpdate` API. It overrides store's `handlers.onContainerUpdate`.

- `onCleanup` _(Function)_: an action that will be triggered after the container has been unmounted and no more consumers of the store instance are present. Useful in case you want to clean up side effects like event listeners or timers. It overrides store's `handlers.onDestroy`.

##### Example

Let's create a container that provides an initial state on tests:

```js
import { createContainer } from 'react-sweet-state';
import { ColorsStore } from './colors';

const ColorsContainer = createContainer(ColorsStore, {
onInit:
() =>
({ setState }, { initialTodos }) => {
setState({ todos: initialTodos });
({ setState }, { initialColor }) => {
setState({ color: initialColor });
},
});

const UserTodos = ({ initialTodos }) => (
<TodosContainer initialTodos={initialTodos}>
<TodosList />
</TodosContainer>
);
it('should render with right color', () => {
const mockColor = 'white';
const { asFragment } = render(
<ColorsContainer initialColor={mockColor}>
<TodosList />
</ColorsContainer>
);
expect(asFragment()).toMatchSnapshot();
});
```
37 changes: 28 additions & 9 deletions docs/api/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,46 @@ createStore(config);

- `name` _(string)_: optional, useful for debugging and to generate more meaningful store keys.

- `containedBy` _(Container)_: optional, specifies the Container component that should handle the store boundary

- `handlers` _(object)_: optional, defines callbacks on specific events

- `onInit` _(Function)_: action triggered on store initialisation
- `onUpdate` _(Function)_: action triggered on store update
- `onDestroy` _(Function)_: action triggered on store destroy
- `onContainerUpdate` _(Function)_: action triggered when `containedBy` container props change

##### Returns

_(Object)_: used to create Containers, hooks and Subscribers related to the same store type
_(Object)_: used to create hooks, Subscribers and override Containers, related to the same store type

##### Example

Let's create a Container that automatically populates the todos' Store instance with some todos coming from SSR, for instance.
Let's create a Store with an action that loads the todos' and triggers it on store initialisation

```js
import { createContainer } from 'react-sweet-state';
import Store from './store';
import { createStore } from 'react-sweet-state';
import { TodosContainer } from './container';

const actions = {
load:
() =>
async ({ setState }) => {
const todos = await fetch('/todos');
setState({ todos });
},
};

const Store = createStore({
name: 'todos',
initialState: { todos: [] },
actions: {
load:
actions,
containedBy: TodosContainer,
handlers: {
onInit:
() =>
async ({ setState }) => {
const todos = await fetch('/todos');
setState({ todos });
async ({ dispatch }) => {
await dispatch(actions.load());
},
},
});
Expand Down
23 changes: 21 additions & 2 deletions docs/basics/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,29 @@ const actions = {
const Store = createStore({ initialState, actions });
```

Optionally, you can add a unique `name` property to the `createStore` configuration object. It will be used as the displayName in Redux Devtools.
Optionally, you can add to the `createStore` configuration object a unique `name` property, a bound to a Container component via `containedBy` property, and a series of `handlers` to trigger actions on specific events.

```js
const Store = createStore({ initialState, actions, name: 'counter' });
const Store = createStore({
initialState,
actions,
name: 'counter',
containedBy: StoreContainer,
handlers: {
onInit:
() =>
({ setState }, containerProps) => {},
onUpdate:
() =>
({ setState }, containerProps) => {},
onDestroy:
() =>
({ setState }, containerProps) => {},
onContainerUpdate:
() =>
({ setState }, containerProps) => {},
},
});
```

The first time a hook or a `Container` linked to this store is rendered, a Store instance will be initialised and its state shared across all components created from the same Store. If you need multiple instances of the same Store, use the `Container` component ([see Container docs for more](../advanced/container.md)).
1 change: 1 addition & 0 deletions docs/recipes/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Recipes

- [State rehydration](./rehydration.md)
- [Flow types](./flow.md)
- [Typescript types](./typescript.md)
- [Composition](./composition.md)
7 changes: 4 additions & 3 deletions docs/recipes/flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ const actions = {
},
};

const CounterContainer: ContainerComponent<{}> = createContainer();

const Store = createStore<State, Actions>({
containedBy: CounterContainer,
initialState,
actions,
});

const CounterSubscriber: SubscriberComponent<State, Actions> =
createSubscriber(Store);
const useCounter: HookFunction<State, Actions> = createHook(Store);
const CounterContainer: ContainerComponent<{}> = createContainer(Store);
```

#### Actions pattern
Expand Down Expand Up @@ -117,6 +119,5 @@ If your container requires additional props:
type ContainerProps = { multiplier: number };

// this component requires props
const CounterContainer: ContainerComponent<ContainerProps> =
createContainer(Store);
const CounterContainer: ContainerComponent<ContainerProps> = createContainer();
```
Loading