react-sweet-state

repository·master·Indexed 21 days ago

https://github.com/atlassian/react-sweet-state

A flexible and scalable state management solution for React that combines concepts from Redux and the Context API. It allows developers to define stores with initial state and actions, which are consumed via custom hooks. Features include support for async actions, Redux Devtools integration, middlewares, and Containers for managing local, global, or scoped store instances. Note: This package is deprecated and tested up to React 18.

Tokens
20.7K
Snippets
65
Records
75
Agent score
65%

What's inside react-sweet-state

  1. Alternative hook creators: createStateHook and createActionsHook

    master

    In addition to the standard createHook which returns both state and actions, react-sweet-state provides two specialized creators:

    • createStateHook: Creates a hook that only returns the state.
    • createActionsHook: Creates a hook that only returns the actions.

    Use these when a component only needs to read data or only needs to trigger updates, to avoid unnecessary re-renders or to simplify the component API.

  2. Triggering actions on other Stores

    master

    Sweet-state does not provide a built-in mechanism to trigger actions on one Store from another. This design encourages a consistent Flux architecture where state changes flow top-down.

    To compose actions across different stores or integrate with other state management libraries, use React hooks. This allows you to orchestrate multiple stores within a component or a custom hook.

  3. Compare sweet-state with React Context API

    master

    While sweet-state uses the React Context API under the hood, it is designed to solve the inherent limitations and common pitfalls of using raw Context at scale.

    Why use sweet-state instead of raw Context API?

    Raw Context API has several limitations that sweet-state addresses:

    • Performance: Context is not designed for frequent updates and has performance limitations regarding re-renders. sweet-state provides mechanisms to prevent unnecessary re-renders.
    • Async Operations: Handling asynchronous operations in raw Context is difficult; sweet-state provides a structured way to handle them.
    • Selectors: Raw Context does not natively support selectors; sweet-state includes selector support to optimize updates.
    • Debuggability: sweet-state integrates with Devtools to improve visibility into state changes.
    • Safety: sweet-state protects consumers from common mistakes related to the asynchronous nature of React setState and provides better handling for provider-less scenarios.
  4. How the action thunk receives arguments

    master

    When you define an action, the inner function (the thunk) is automatically called by sweet-state with the following arguments:

    1. State Utilities Object: An object containing:
      • setState: Function to update the store state.
      • getState: Function to retrieve the current store state.
      • dispatch: Function to dispatch actions.
    2. Container Props Object: An object containing the custom props defined on the Container component.
  5. How react-sweet-state works

    master

    The library combines Redux and React Context API concepts. It uses Stores to hold state and actions, and Hooks (or Subscribers) to consume them.

    Core Concepts

    • Store: Defined by an initialState and a set of actions. It acts as the single source of truth for a specific piece of state.
    • Actions: Functions that receive an object containing getState and setState. This is similar to Redux thunks.
    • setState: By default, setState performs a shallow merge of the provided object with the current state (similar to React's setState). You can replace this with custom logic (e.g., using immer).
    • Hooks: Created via createHook(Store), these allow components to subscribe to the store. A hook automatically handles the instantiation of the store if it hasn't been created yet, making state sharing easy.
  6. Compare sweet-state with Redux

    master

    sweet-state is influenced by Redux but is designed to make modern React patterns first-class citizens. Use sweet-state if you want to avoid the 'all-or-nothing' state ownership model of Redux and prefer a library that promotes composition and integration with other state management solutions (like Apollo or GraphQL).

    Shared Features

    • Debuggability: Both support Redux Devtools.
    • Selectors: Both support state selectors.
    • Testability: Both use similar testing strategies for actions and selectors.

    Key Differences

    • Interoperability: Redux works best when it owns the entire state; sweet-state is designed to be composed with other solutions.
    • Boilerplate: sweet-state uses a pattern similar to redux-thunk actions but eliminates the need for reducers, significantly reducing boilerplate.
    • Nesting: Unlike Redux, which relies on a Provider that can make component nesting challenging, sweet-state has no Provider concept, allowing components to be more resilient and easily nested/mixed.
  7. How Containers work in react-sweet-state

    master

    While react-sweet-state promotes independent micro-Stores, using a single global instance for each Store type can be limiting. Container components solve this by allowing you to create multiple independent instances of the same Store type.

    Containers can be used in three ways:

    1. Locally scoped (Default): Creates a Store instance accessible only to its children. These instances are automatically cleaned up when the last Container accessing them unmounts.
    2. Global singleton (isGlobal): Acts as a transparent link to the "default" global instance of that Store type.
    3. Scoped global (scope): Creates or retrieves a global instance prefixed with a specific scope ID. This allows you to have multiple distinct global instances (e.g., counter-1, counter-2) that can be shared across different parts of the component tree.

    You can change the scope of a Store (from local to global or scoped) by moving the Container in the tree or adding props, without needing to modify the child components or hooks.

    import { createStore, createContainer, createHook } from 'react-sweet-state';
    
    export const CounterContainer = createContainer();
    
    const Store = createStore({
      containedBy: CounterContainer,
      initialState: { count: 0 },
      actions: {
        increment: () => ({ setState }) => {
          const currentCount = getState().count;
          setState({ count: currentCount + 1 });
        },
      },
    });
    
    const useCounter = createHook(Store);
    
    export const CounterButton = () => {
      const [{ count }, { increment }] = useCounter();
      return <button onClick={increment}>{count}</button>;
    };
  8. Understand the design philosophy of sweet-state

    master

    sweet-state is designed to solve the complexity of React state management by combining the strengths of three major patterns: Redux, React Context, and React Hooks.

    It aims to provide:

    • Redux-like features: Actions, Middlewares, Selectors, and high performance/Devtools support.
    • Context-like flexibility: No pre-determined boundaries, the ability to use state without a Provider (provider-less), and explicit render props.
    • Hooks-like ergonomics: Ease of use and predictability.
  9. Implement async actions in Sweet-state

    master

    Store actions can be asynchronous. You can perform side effects (like API calls) and call setState multiple times within a single action to update the state throughout the lifecycle of the operation (e.g., setting a loading flag to true before a fetch and false after).

    Important: Because state changes are applied immediately, always use getState() to retrieve the most current state value if you need to perform logic based on the state between consecutive setState calls within the same async action.

    const actions = {
      load:
        () =>
        async ({ setState, getState }, { api }) => {
          if (getState().loading === true) return;
    
          setState({
            loading: true,
          });
    
          const todos = await api.get('/todos');
    
          setState({
            loading: false,
            data: todos,
          });
        },
    };
  10. Basic TypeScript setup for react-sweet-state

    master

    To use react-sweet-state with TypeScript, define your State and Actions types. Use the Action<State> type for your action implementations. Most create* methods (like createStore, createSubscriber, and createHook) can infer their generics from the Store instance, so manual typing is often unnecessary.

    Key steps:

    1. Define the State type.
    2. Define actions using the Action<State> type.
    3. Create a Container using createContainer().
    4. Initialize the Store with createStore<State, Actions>({ initialState, actions, containedBy: Container }).
    5. Derive hooks and subscribers from the Store.
    import {
      createStore,
      createSubscriber,
      createHook,
      createContainer,
      Action,
    } from 'react-sweet-state';
    
    type State = { count: number };
    type Actions = typeof actions;
    
    const initialState: State = {
      count: 0,
    };
    
    const actions = {
      increment:
        (by = 1): Action<State> =>
        ({ setState, getState }) => {
          setState({
            count: getState().count + by,
          });
        },
    };
    
    const CounterContainer = createContainer();
    
    const Store = createStore<State, Actions>({
      initialState,
      actions,
      containedBy: CounterContainer,
    });
    
    const CounterSubscriber = createSubscriber(Store);
    const useCounter = createHook(Store);
  11. Typing createHook and createSubscriber with selectors

    master

    When using a selector function with createHook or createSubscriber, you must adjust the generic arguments of the resulting type to reflect the selector's output and its required props.

    Without Selector Props

    If the selector only takes state, the type arguments are (SelectorState, Actions):

    type SelectorState = boolean;
    const selector = (state: State): SelectorState => state.count > 0;
    
    const useCounter: HookFunction<SelectorState, Actions> = createHook(Store, {
      selector,
    });

    With Selector Props

    If the selector takes state and props, the type arguments are (SelectorState, Actions, SelectorProps):

    type SelectorProps = { min: number };
    type SelectorState = boolean;
    const selector = (state: State, props: SelectorProps): SelectorState => state.count > props.min;
    
    const useCounter: HookFunction<SelectorState, Actions, SelectorProps> = createHook(Store, {
      selector,
    });