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

Dynamic container implementation #196

Closed
wants to merge 5 commits into from
Closed
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
65 changes: 65 additions & 0 deletions docs/api/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,68 @@ const UserTodos = ({ initialTodos }) => (
</TodosContainer>
);
```

### createDynamicContainer

```js
createDynamicContainer(config);
```

##### Arguments

1. `config` _(Object)_: containing one or more of the following keys:

- `displayName` _(string)_: used by React to better identify a component. Defaults to `DynamicContainer`

- `matcher` _(Function)_: a function returning `true` for stores that need to be contained. Required.

- `onStoreInit` _(Function)_: an action that will be triggered on each store initialisation. If you define multiple containers sharing the same scope, this action will still only be run **once** by one of the container components, so ensure they receive the same props.

- `onStoreUpdate` _(Function)_: an action that will be triggered when a store state changes.

Choose a reason for hiding this comment

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

I think we should include the function parameters here. Especially for this one, it is not clear how I will know which store instance is changing state? Just by looking at the documentation, it is not clear. Same for the other function configs as well.


- `onStoreCleanup` _(Function)_: an action that will be triggered after a store is no longer listened to (usually after container unmounts). Useful in case you want to clean up side effects like event listeners or timers. As with `onStoreInit`, if you define multiple containers this action will trigger by the **last one** unmounting.

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

##### 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:

- `isGlobal` _(bool)_: by default, Container defines a local store instance. This prop will allow child components to get data from the global store's registry instance instead

- `scope` _(string)_: this option will allow creating multiple global instances of the same store. Those instances will be automatically cleaned once all the containers pointing to the scoped version are removed from the tree. Changing a Container `scope` will: create a new Store instance, make `onInit` action run again and all child components will get the data from it.

Choose a reason for hiding this comment

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

Which onInit? Is that a typo?


- `...props` _(any)_: any additional prop set on the Container component is made available in the actions of child components

##### Example

Let's create a Container that contains and initializes all theme-related stores, for instance.

```js
import { createDynamicContainer } from 'react-sweet-state';

// Assume we have a ColorsStore and a FontSizesStore with `tags: ['theme']`

const ThemeContainer = createDynamicContainer({
matcher: (Store) => Store.tags.includes('theme'),

Choose a reason for hiding this comment

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

How do I define the tags here? Need something in the docs IMO, currently only way to know is digging the example files.

onStoreInit:
() =>
({ setState, getState }, { initialTheme }) => {
const state = getState();
if ('colors' in state) {

Choose a reason for hiding this comment

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

Instead of duck typing the state, wouldn't be better to check the store against some reference. b/c the parameters to the action creator are not clear here, I am not sure how.

// refining to colors store
setState({ colors: initialTheme.colors });
}
if ('fontSizes' in state) {
// refining to sizes store
setState({ sizes: initialTheme.sizes });
}
},
});

const UserTheme = ({ colors, sizes }) => (
<ThemeContainer initialTheme={{ colors, sizes }}>
<TodosList />
</ThemeContainer>
);
```
29 changes: 29 additions & 0 deletions examples/advanced-scoped-dynamic/components/color.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
createStore,
createHook,
type StoreActionApi,
} from 'react-sweet-state';

type State = {
color: string;
};

const initialState: State = {
color: 'white',
};

const actions = {
set:
(color: string) =>
({ setState }: StoreActionApi<State>) => {
setState({ color });
},
};

const Store = createStore({
initialState,
actions,
tags: ['theme'],
});

export const useColor = createHook(Store);
29 changes: 29 additions & 0 deletions examples/advanced-scoped-dynamic/components/width.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
createStore,
createHook,
type StoreActionApi,
} from 'react-sweet-state';

type State = {
width: number;
};

const initialState: State = {
width: 200,
};

const actions = {
set:
(width: number) =>
({ setState }: StoreActionApi<State>) => {
setState({ width });
},
};

const Store = createStore({
initialState,
actions,
tags: ['theme'],
});

export const useWidth = createHook(Store);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>

<head>
<title>Basic example with Flow</title>
<title>Advanced dynamic scoped example</title>
<style>
body { font-family: sans-serif; }
main { display: flex; line-height: 1.5; }
Expand Down
59 changes: 59 additions & 0 deletions examples/advanced-scoped-dynamic/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { createDynamicContainer } from 'react-sweet-state';

import { useColor } from './components/color';
import { useWidth } from './components/width';

const ThemingContainer = createDynamicContainer({
matcher: (Store) => Store.tags?.includes('theme') ?? false,
});

const colors = ['white', 'aliceblue', 'beige', 'gainsboro', 'honeydew'];
const widths = [200, 220, 240, 260, 280];
const rand = () => Math.floor(Math.random() * colors.length);

/**
* Components
*/
const ThemeHook = ({ title }: { title: string }) => {
const [{ color }, { set: setColor }] = useColor();
const [{ width }, { set: setWidth }] = useWidth();

return (
<div style={{ background: color, width }}>
<h3>Component {title}</h3>
<p>Color: {color}</p>
<p>Width: {width}</p>
<button onClick={() => setColor(colors[rand()])}>Change color</button>
<button onClick={() => setWidth(widths[rand()])}>Change width</button>
</div>
);
};

/**
* Main App
*/
const App = () => (
<div>
<h1>Advanced dynamic scoped example</h1>
<main>
<ThemingContainer scope="t1">
<ThemeHook title="scope" />
</ThemingContainer>
<ThemingContainer>
<ThemeHook title="local" />
</ThemingContainer>
<ThemingContainer scope="t1">
<ThemeHook title="scope sync" />
</ThemingContainer>
</main>
</div>
);

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<StrictMode>
<App />
</StrictMode>
);
40 changes: 0 additions & 40 deletions examples/basic-flow/components.js

This file was deleted.

34 changes: 0 additions & 34 deletions examples/basic-flow/index.js

This file was deleted.

1 change: 0 additions & 1 deletion examples/basic-ts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @flow
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';

Expand Down
12 changes: 11 additions & 1 deletion examples/performance-scale-test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom/client';
import { useTodo } from './controllers/todos';

const COLLECTION = Array.from({ length: 500 });
const ONE_EVERY = 10;

type TodoViewProps = { id: string, count: number };

Expand All @@ -27,13 +28,22 @@ const TodoView = ({ id, count }: TodoViewProps) => {
*/
const App = () => {
const [count, setCount] = useState(0);

useEffect(() => {

Choose a reason for hiding this comment

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

I believe this is an unrelated change. Ideally would be better if we can exclude from this PR.

setTimeout(() => setCount((c) => c + 1));
});

return (
<div>
<h1>Performance</h1>
<button onClick={() => setCount(count + 1)}>Trigger</button>
<main>
{COLLECTION.map((v, n) => (
<TodoView key={n} id={String(n)} count={count} />
<TodoView
key={n}
id={String(n)}
count={Math.floor(count / ONE_EVERY)}
/>
))}
</main>
</div>
Expand Down
6 changes: 0 additions & 6 deletions src/__tests__/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,3 @@ export const storeStateMock = {
listeners: () => [],
mutator: () => {},
};

export const registryMock = {
configure: jest.fn(),
getStoreState: jest.fn(),
deleteStoreState: jest.fn(),
};
2 changes: 2 additions & 0 deletions src/components/__tests__/container.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createSubscriber } from '../subscriber';
const mockLocalRegistry = {
configure: jest.fn(),
getStore: jest.fn(),
hasStore: jest.fn(),
deleteStore: jest.fn(),
};

Expand All @@ -21,6 +22,7 @@ jest.mock('../../store/registry', () => ({
defaultRegistry: {
configure: jest.fn(),
getStore: jest.fn(),
hasStore: jest.fn(),
deleteStore: jest.fn(),
},
}));
Expand Down
Loading