Giter Site home page Giter Site logo

Comments (2)

joshuatvernon avatar joshuatvernon commented on August 20, 2024

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.

anacierdem avatar anacierdem commented on August 20, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.