Comments (2)
I did this recently albeit not as elegantly as I would have liked. Here's the code. It works great, however, typing everything is pretty horrible
import {
BoundActions,
createContainer as createReactSweetStateContainer,
createHook as createReactSweetStateHook,
createStore as createReactSweetStateStore,
createSubscriber as createReactSweetStateSubscriber,
defaults,
Store
} from 'react-sweet-state';
import isNil from 'lodash/isNil';
import { UndefinedOr } from '../base-models/generics';
import { baseServices } from '../services';
import { isBrowser } from '../utils';
import { mergeWithEmpty } from '../utils/lodash';
import { BaseActions, baseActions } from './actions';
import { getInitialState, InitialStateScript } from './components';
import { registry } from './registry';
import { baseSelectors } from './selectors';
import { baseState } from './state';
import {
Actions,
BaseState,
ContainerType,
SelectorProps,
Selectors,
SelectorState,
Services,
Stage,
State,
SubscriberType,
UseStore
} from './types';
// Override the mutator that is called by `setState` within `actions` so the we don't have to spread to perform deep merges
defaults.mutator = (currentState, partialState) => mergeWithEmpty(currentState, partialState);
export const DEFAULT_STORE_NAME = 'base-store';
// The following variables are used as singletons
let store: UndefinedOr<Store<any, any>>;
let selectors: Selectors;
let services: Services;
// `useStoreInstance` is wrapped by `useStore` which accepts generics
// so that typescript can protect custom state and custom actions
let useStoreInstance: UseStore<any, any>;
let Subscriber: SubscriberType<any, any>;
let Container: ContainerType;
/**
* Create and return a `useStore` hook for accessing the passed in `store`
* @param store `store` used to create a `useStore` hook
*/
export const createHook = <S extends {} = State, A extends {} = Actions>(store: Store<S, A>): UseStore<S, A> =>
createReactSweetStateHook<S, A, SelectorState, SelectorProps<S>>(store);
/**
* Create and return a `Subscriber` for accessing the passed in `store`
* @param store `store` used to create a `Subscriber`
*/
export const createSubscriber = <S extends {} = State, A extends {} = Actions>(store: Store<S, A>) =>
createReactSweetStateSubscriber<S, A, SelectorState, SelectorProps<S>>(store);
/**
* Create and return a `Container` for accessing the passed in `store`
* @param store `store` used to create a `Container`
*/
export const createContainer = <S extends {} = State, A extends {} = Actions>(store: Store<S, A>) =>
createReactSweetStateContainer<S, A, SelectorProps<S>>(store, {
onInit: () => ({ setState }, { initialState }) => setState(initialState)
});
/**
* Creates (or overrides) and returns the `store` and initializes (or overrides) `selectors`,
* `services`, `useStore`, `Subscriber` and `Container` for interacting with the `store`
*
* NOTE: If `createStore` is not called then the `useStore`, `Subscriber` and `Container` will
* access the default **ADGLPWebAppLibrary** `store`
*
* @param name `name` used for the store (Used in Redux Dev Tools)
* @param initialState `initialState` used to set `store used to create a `Container`
* @param initialActions `initialActions` used to update `store`
* @param initialSelectors `initialSelectors` used to retrieve state from the `store`
* @param initialServices `initialServices` used by actions to retrieve data to update state in the `store`
*/
export const createStore = <S extends {} = State, A extends {} = Actions>(
name: string,
initialState: S,
initialActions: A,
initialSelectors: Selectors,
initialServices?: Services
) => {
// Initialize (or override) the store
store = createReactSweetStateStore<S, A>({
initialState,
name,
actions: initialActions
});
// Initialize (or override) the selectors
selectors = initialSelectors;
// Initialize (or override) the services
services = initialServices ?? baseServices;
// Initialize (or override) the hook for accessing the store
useStoreInstance = createHook<S, A>(store);
// Initialize (or override) the subscriber for accessing the store
Subscriber = createSubscriber<S, A>(store);
// Initialize (or override) the container for accessing the store
Container = createContainer<S, A>(store);
return store;
};
/**
* Initializes `store` with the DEFAULT `baseState`, `actions` and `name`
*
* NOTE: will be overridden if `createStore` is called from tenant web app
*/
if (isNil(store)) {
createStore<BaseState, BaseActions>(
DEFAULT_STORE_NAME,
baseState,
baseActions,
baseSelectors,
baseServices,
baseState.config.stage
);
}
/**
* `useStore` can be used to access state and bound actions within functional components
*/
const useStore = <S extends {} = State, A extends {} = Actions>(): [S, BoundActions<S, A>] => useStoreInstance();
// Export all `store` components so they can be imported from this top level directory directly
export * from './selectors';
export * from './actions';
export * from './state';
export * from './types';
export { store, selectors, services, registry, Subscriber, useStore, Container, InitialStateScript, getInitialState };
Here are some of the generic types. I had to create RecursiveExtendedPartial
to solve the case that I wanted to allow new properties at any level whilst retaining type safety for any base state properties . . .
export type ExtendedPartial<T> = {
[P in keyof T]?: T[P];
} &
Any;
/**
* Recursively:
* - Makes all properties optional
* - Allows new properties
* - Retains type checking if original properties are used
*/
export type RecursiveExtendedPartial<T> =
| ({
[P in keyof T]?: RecursiveExtendedPartial<T[P]>;
} & { [key: string]: any })
| T;
export interface BaseState {
// whatever your base state would be . . .
}
export type State = RecursiveExtendedPartial<BaseState>;
export type Actions = ExtendedPartial<BaseActions>;
export type Selectors = ExtendedPartial<BaseSelectors>;
export type Services = ExtendedPartial<BaseServices>;
export type SelectorState = any;
export type SelectorProps<S = State> = {
initialState: S;
};
export type UseStore<S = State, A extends Record<string, ActionThunk<S, A>> = Actions> = (
...args: any
) => [any, BoundActions<S, A>];
export type SubscriberType<S = State, A extends Record<string, ActionThunk<S, A>> = Actions> = SubscriberComponent<
S,
BoundActions<S, A>,
any
>;
export type ContainerType = ContainerComponent<any>;
export type StoreApi = StoreActionApi<BaseState>;
export type ExtendedPartialStoreApi<S = State> = StoreActionApi<S>;
...and here is the extended store code...
import { Container, createStore, Stage, Subscriber, useStore } from '@my-base-state-package';
import { services } from '../services';
import { actions } from './actions';
import { selectors } from './selectors';
import { state } from './state';
import { ExtendedActions, ExtendedState } from './types';
const STORE_NAME = 'extended-store';
// Passing in state, actions, selectors and services here allows them to override the defaults for the store and components using the base store will get the newly overridden versions
const store = createStore<ExtendedState, ExtendedActions>(STORE_NAME, state, actions, selectors, services);
export { store, state, actions, selectors, useStore, Subscriber, Container };
export * from './types';
from react-sweet-state.
We have successfully used instances of the same store to share a functionality between different places without the need for "extending" a store, many times. I think extending a store is fundamentally against how react-sweet-state is constructed; i.e it will look more like a "mixin" as there won't be any straightforward way to set explicit state/function overrides without significant changes to the library.
I understand, as of today, we can reuse actions across different stores. But this might not be a good idea in terms of coupling code. An action could be doing the exactly same thing today, but we never know it might now tomorrow.
I think this is the very reason a mixin or "extended store" is not a solution to this same problem. The base store can change without the extended store even being noticing it. Using a type system will help but it will also help with those shared actions.
I think current ability to share actions and container instances (via scopes) is a pretty powerful tool that does not need an extra layer of store extension rules. It already provides a tool for combining different stores and behaviours via React's well accepted composition tools. On the other hand the current system is not perfect and sharing the behaviour and data is not straightforward. But there are solutions to the problem without breaking the "compositional" structure. See #146 if you want to provide support for improving the way it works 😊
from react-sweet-state.
Related Issues (20)
- The useHook with selector does not trigger rendering properly HOT 3
- [Documentation request] - Example with Next.js SSR
- Possibly incorrect type definition for `ActionThunk` HOT 2
- Incorrect selector behaviour with promises
- Input caret will jump to end of string when using store HOT 4
- better type/name for dispatched actions HOT 1
- Persistent global scoped stores HOT 2
- "Families" containers HOT 7
- Ability to pass callback function during store creation for store initialisation and updates HOT 3
- Non global states HOT 4
- Container cleanup occurs after remounting, clearing state set by onInit HOT 1
- Hook mount/unmount time depends at number of hooks on page HOT 1
- New major HOT 2
- Move from .js to .ts HOT 1
- Uncaught TypeError: Cannot read properties of null (reading 'unsubscribe') HOT 6
- Unstable scheduling behavior
- Unexpected change in behaviour beween React 17 and 18 HOT 14
- Discussion: Should an error in onUpdate trigger the container's error boundary? HOT 3
- Using container on update hook causes React 16 errors to be printed HOT 5
- createActions helper HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from react-sweet-state.