easy-peasy

repository·master·Indexed 26 days ago

https://github.com/ctrlplusb/easy-peasy

A developer-experience-focused abstraction of Redux for React that provides a zero-configuration, hook-based API for state management. It maintains Redux's architectural guarantees and ecosystem compatibility while offering built-in support for derived state, API calls, and full TypeScript type safety. Version 7 modernizes the library for React 19, introducing support for concurrent primitives such as useStoreTransition, useStoreDeferredState, and useStoreOptimistic.

Tokens
47.8K
Snippets
146
Records
232
Agent score
89%

What's inside easy-peasy

  1. Overview of Easy Peasy TypeScript API

    master

    Easy Peasy provides primary types designed to be used when defining your model interfaces. For most use cases, you only need to explicitly import and use these types during the model definition phase to ensure type safety across your stores, actions, and models.

    If you are new to using Easy Peasy with TypeScript, it is recommended to follow the TypeScript tutorial before diving into the API reference.

  2. Introduction to Easy Peasy

    master

    Easy Peasy is an abstraction of Redux designed to improve developer experience by providing a reimagined API for state management. It allows for quick and easy state management while maintaining the architectural guarantees and ecosystem compatibility of Redux.

    Key features include:

    • No configuration required: Works out of the box for robust and scalable state management.
    • Advanced features: Built-in support for derived state and API calls.
    • Developer Experience: Includes integration with Redux Developer Tools.
    • Type Safety: Provides a fully typed experience via TypeScript.
  3. Evaluate state management alternatives to Easy Peasy

    master

    Easy Peasy is one of several state management options for React. Before choosing Easy Peasy, consider if your requirements are better met by other specialized libraries:

    • Server State Synchronization: If your primary goal is managing data synchronization with a server (e.g., handling GET/POST requests and CRUD operations), consider React Query. It is designed to reduce complexity and prevent bugs related to client/server data synchronization.
    • Native React APIs: For many use cases, React's built-in Context and Hook APIs may be sufficient and offer long-term stability as they are part of the core library.
    • Lightweight Global State: Zustand provides an elegant API for global state management and is designed to handle common pitfalls like the 'zombie child' problem and React concurrency.
    • Redux Ecosystem: Redux Toolkit provides a simplified API for Redux to reduce boilerplate. Use this if you want to leverage the extensive Redux ecosystem and community support (requires react-redux to connect to your application).
  4. Use easy-peasy/server for React-free environments

    master

    Use the easy-peasy/server subpath export in environments where importing React is incorrect or undesirable, such as:

    • React Server Components (RSC)
    • Edge runtimes
    • Node.js scripts (e.g., for building store snapshots without a component tree)

    This subpath provides the full store and model API but excludes all React-bound hooks and the StoreProvider. This allows you to build stores, dispatch actions, and read state without pulling React into your server-side bundle.

  5. Understand the Easy Peasy architecture

    master

    Easy Peasy is a full abstraction layer over Redux. It provides an intuitive API designed to eliminate boilerplate while leveraging the mature Redux ecosystem.

    Key architectural benefits include:

    • Redux Compatibility: Since Easy Peasy outputs a standard Redux store, it is fully compatible with the Redux DevTools Extension out of the box.
    • Interoperability: You can use Easy Peasy alongside existing libraries and applications that use react-redux.
    • Gradual Migration: It supports a gradual migration strategy from standard React Redux to Easy Peasy.
    • Extensibility: You can extend the underlying Redux store using standard Redux middleware and enhancers via configuration options.

    Note: While built on Redux, no prior Redux experience is required to use Easy Peasy.

  6. Understand the tradeoffs and downsides of easy-peasy

    master

    When deciding whether to use easy-peasy, consider the following architectural tradeoffs:

    • Redux Dependency: easy-peasy is built on top of Redux. While this provides access to battle-tested optimizations and the Redux DevTools ecosystem, you are bound to the capabilities and API of Redux.
    • Leaky Abstraction: The abstraction is intentionally "leaky." Knowledge of Redux will help you understand easy-peasy more deeply. The library specifically exposes the ability to extend the underlying Redux store and uses Redux-specific concepts (like reducers) to allow for migration paths from traditional Redux implementations.
    • Third-party Risk: As a third-party dependency, there is a risk regarding maintenance and alignment with evolving React architectures (e.g., React Concurrent Mode). However, the library is designed to evolve alongside React features like useSyncExternalStore (formerly useMutableSource).
    • Bundle Size: Using easy-peasy increases your bundle size. The library, including its dependencies, is approximately 11kb gzipped.
    • Onboarding: New developers will need to learn the easy-peasy API. While the API is designed to be intuitive, it does not have the same level of widespread community adoption as Redux or MobX.
  7. Quickstart: Create, Wrap, and Use a Store

    master

    To use Easy Peasy in your application, follow these three steps:

    1. Create your store using createStore and define your state and actions using action.
    2. Wrap your application with the StoreProvider component, passing in your created store.
    3. Use the store in your components using hooks like useStoreState to access state and useStoreActions to access actions.

    Note: The v7 Beta maintains the same public store/model API as v6.

    // 1. Create your store
    const store = createStore({
      todos: ['Create store', 'Wrap application', 'Use store'],
    
      addTodo: action((state, payload) => {
        state.todos.push(payload);
      }),
    });
    
    // 2. Wrap your application
    function App() {
      return (
        <StoreProvider store={store}>
          <TodoList />
        </StoreProvider>
      );
    }
    
    // 3. Use the store
    function TodoList() {
      const todos = useStoreState((state) => state.todos);
      const addTodo = useStoreActions((actions) => actions.addTodo);
      return (
        <div>
          {todos.map((todo, idx) => (
            <div key={idx}>{todo}</div>
          ))}
          <AddTodo onAdd={addTodo} />
        </div>
      );
    }
  8. Test thunks using Strategy 1: Mocking actions

    master

    This strategy prevents dispatched actions from executing and instead records them. Use this to verify that a thunk dispatches the correct actions with the correct payloads without triggering side effects from those actions.

    To use this, set mockActions: true in the createStore configuration. You can then inspect the recorded actions using store.getMockedActions().

    import { createStore } from 'easy-peasy';
    
    // ... model definition ...
    
    test('fetchById', async () => {
      // arrange
      const todo = { id: 1, text: 'Test my store' };
      const mockTodosService = {
        fetchById: jest.fn(() => Promise.resolve(todo)),
      };
      const store = createStore(todosModel, {
        injections: { todosService: mockTodosService },
        mockActions: true,
      });
    
      // act
      await store.getActions().fetchById(todo.id);
    
      // assert
      expect(mockTodosService.fetchById).toHaveBeenCalledWith(todo.id);
      expect(store.getMockedActions()).toEqual([
        { type: '@thunk.fetchById(start)', payload: todo.id },
        { type: '@action.fetchedTodo', payload: todo },
        { type: '@thunk.fetchById(success)', payload: todo.id },
        { type: '@thunk.fetchById', payload: todo.id },
      ]);
    });
  9. Persist state using the persist API

    master

    The persist API allows you to save your store state (e.g., to sessionStorage). Easy Peasy automatically rehydrates the state from storage when the store is created.

    Because rehydration is asynchronous, use the useStoreRehydrated hook to ensure the state is ready before rendering dependent components. Wrap the dependent part of your tree in a React <Suspense> boundary.

    import { Suspense } from 'react';
    import { createStore, action, persist, useStoreRehydrated } from 'easy-peasy';
    
    const model = {
      count: 1,
      inc: action((state) => {
        state.count += 1;
      }),
    };
    
    const store = createStore(persist(model));
    
    function Main() {
      // This hook suspends the component until rehydration is complete
      useStoreRehydrated();
      return <App />;
    }
    
    function Root() {
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <Main />
        </Suspense>
      );
    }
  10. Define injections at runtime

    master

    You can provide or update injections (external values used by your store) at runtime via the Provider.

    • To override all previous injections: Pass an object to the injections prop.
    • To update existing injections: Pass a function to the injections prop. This function receives the previousInjections as an argument, allowing you to spread them and overwrite specific keys.
    import CounterStore from './stores/counter';
    
    function MyApp({ language }) {
      const translator = useTranslator(language);
    
      return (
        <>
          <Header />
          {/* Option 1: Overwrite all injections */}
          <CounterStore.Provider injections={{ translator }}>
            <Main />
          </CounterStore.Provider>
    
          {/* Option 2: Update existing injections using a function */}
          <CounterStore.Provider
            injections={(previousInjections) => ({
              ...previousInjections,
              translator,
            })}
          >
            <Main />
          </CounterStore.Provider>
        </>
      );
    }