Introduction

pinia-react is a React state management library inspired by Vue's Pinia. It's built upon Pinia's core code and leverages React Hooks and useSyncExternalStore to provide a concise, reactive, and TypeScript-friendly state management experience.

It's a React adaptation of Pinia, based on a portion of Pinia's core code and optimized for the React ecosystem (e.g., using useSyncExternalStore to support React 18's concurrent rendering). We strictly adhere to Pinia's MIT license and have retained the original author's copyright information in the license file.

Why You Should Use Pinia-React

Pinia-React, as the React adaptation of Pinia, allows you to share state across components or pages. It automatically tracks state dependencies and only updates the necessary components. It's important to note that components don't re-render just because the store's data changes; it collects dependencies on-demand. For example, if a store has two pieces of data, count and name, and your component only uses count, then your component will only re-render when count changes.

Basic Example

Here's a basic example of the Pinia API (to continue reading this introduction, please make sure you've already read the Getting Started section). First, you can create a Store:

// stores/counter.js
import { defineStore } from 'pinia-react'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // You can also define it like this
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

Then, you can use the store in a component:

export function App() {

  const store = useCounterStore()
  counter.count++
  // Autocomplete! ✨
  counter.$patch({ count: counter.count + 1 })
  // Or use an action instead
  counter.increment()

  return (
    <div>Current Count: {{ counter.count }}</div>
  )
}

Differences from Pinia

  • Pinia-React only supports the options store style and does not have the setup store style.
  • There is currently no test utility suite.
  • There is currently no Devtools support.
  • There is currently no hot reloading support.
  • There are no Vue-specific helper functions for mapping state.

Comparison

There is a wide variety of React state management libraries, each with its own strengths. Pinia-React is one of them. Here, we'll mainly compare it with Zustand. For Redux, Valtio, Jotai, and Recoil, you can refer to the Zustand documentation.

State Model

Pinia-React and Zustand take fundamentally different approaches to state management. Pinia-React is based on a mutable state model, while Zustand is based on an immutable state model.

Pinia-React

import { defineStore, createPinia } from 'pinia-react';

// Use this in your entry file.
const pinia = createPinia();

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});

Zustand

import { create } from 'zustand';

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
}

export const useCounterStoreZustand = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

Render Optimization

Another difference between Pinia-React and Zustand is that Pinia-React automatically collects dependencies for render optimization, whereas Zustand requires you to manually select dependencies for render optimization using selectors.

Pinia-React

import React from 'react';
import { useCounterStore } from './counterStore';

export function CounterWithPinia() {
  const counter = useCounterStore();

  return (
    <div>
      <h2>Pinia-React Count: {counter.count}</h2>
      <button onClick={() => counter.increment()}>Increase</button>
      <button onClick={() => counter.decrement()}>Decrease</button>
    </div>
  );
}

Zustand

import React from 'react';
import { useCounterStoreZustand } from './counterStore';

export function CounterWithZustand() {
  const count = useCounterStoreZustand((state) => state.count);
  const increment = useCounterStoreZustand((state) => state.increment);
  const decrement = useCounterStoreZustand((state) => state.decrement);

  return (
    <div>
      <h2>Zustand Count: {count}</h2>
      <button onClick={increment}>Increase</button>
      <button onClick={decrement}>Decrease</button>
    </div>
  );
}