-
Notifications
You must be signed in to change notification settings - Fork 55
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
- `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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which |
||
|
||
- `...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'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
``` |
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); |
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 |
---|---|---|
@@ -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> | ||
); |
This file was deleted.
This file was deleted.
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'; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 }; | ||
|
||
|
@@ -27,13 +28,22 @@ const TodoView = ({ id, count }: TodoViewProps) => { | |
*/ | ||
const App = () => { | ||
const [count, setCount] = useState(0); | ||
|
||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
There was a problem hiding this comment.
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.