React状态管理后起之秀Zustand

相比Redux可简单太多了,Redux相对啰嗦,其次就是资源占用问题,单单引入一个状态管理的功能来说,相对臃肿,这也是一直被社区所诟病 的问题,相信使用过zustand之后回头再看看Redux, 大概这就是后浪吧

安装

npm install zustand 
# 或者
yarn add zustand

zustand的基本使用

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
  const { count, inc } = useStore()
  return (
    <div>
      <span>{count}</span>
      <button onClick={inc}>one up</button>
    </div>
  )
}

防止重新渲染,比如更新某个值相邻的操作之间值不变也触发了重新渲染,这种就需要加个处理

import { shallow } from "zustand/shallow";

// 原来是这样
const counter = useCounter((state) => state.counter);

// 优化之后是这样, 据说这样就会触发比较:  (previousCounter, nextCounter) => previousCounter === nextCounter;
const counter = useCounter((state) => state.counter, shallow);

异步操作,比如网络请求刷新状态

const useCounter = create((set) => {
    return {
        counter: 0,
        incrCounter: async () => {
            const { data } = await axios.get("/counter");
            set({
                counter: data.counter,
            });
        },
    };
});

模块化使用

personSlice.js

//需要管理的状态变量以及操作方法均放在一个箭头函数里返回一个对象
export const PersonSlice = (set) => ({
    name: 'zustand',
    age: 18,
    add: () => set((state) => ({ age: state.age + 1 })),
    resetZero: () => set({ age: 0 }),
});

store.js

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

///将不同类型的状态按职责区分在不同的文件然后导出汇聚到这里处理, 就是模块化的意思
import { PersonSlice } from './personSlice.js';

/// 这里定义状态和操作方法
// const createStore = (set) => ({
//   count: 0,
//   //网络请求异步操作
//   fishies: {},
//   fetch: async (pond) => {
//     const response = await fetch(pond);
//     set({ fishies: await response.json() });
//   },
// });

///调试和持久化
// devtools 是对调试工具的支持
// persist 是保存持久化的中间件,刷新时添加到状态的数据不会丢失 是存在localStorage里的 name就是key value就是这个store的对象
//不需要时可以加个方法进行清除localStorage的数据 
// 这里只是一个举例,实际开发中store可以建多个,可以按持久化和非持久化来划,也可按模块划分
const useStore = create(
  devtools(
    persist(
      (...a) => ({
        //类似Redux Slice切片  切片获取状态管理的对象进行解构组装
        // ...createSlice(...a),
        ...PersonSlice(...a),
      }),
      { 
        name: 'store' , //唯一key      
        getStorage: () => sessionStorage, // (可选)默认使用'localStorage'
			}  
    )
  )
);
export default useStore;

index.jsx

import React from 'react';
import useAppStore from '../store.js';

/// 状态管理示例
function StoreExample (){
  //取出状态管理的变量进行展示 
  //const {age,name} = useAppStore();
    const age = useAppStore((state) => state.age);
    const name = useAppStore((state) => state.name);
  return (
    <div>
      <div> age: {age}</div>
      <div> name: {name}</div>
    </div>
  );
}

function ActionExample (){
  //取出操作方法进行调用 
  //const { add, resetZero } = useAppStore();
    const add = useAppStore((state) => state.add);
    const resetZero = useAppStore((state) => state.resetZero);


  return (
    <div>
      <button onClick={add}>Add</button>
      <button onClick={resetZero}>Reset</button>
    </div>
  );
}

export default function Index (){
  return (
    <>
      <StoreExample/>
      <ActionExample/>
    </>
  );
}

一个todos的案例

import create from "zustand";

//定义store
const useStore = create((set) => ({
    todos: [],
    addTodo: (text) =>
        set((state) => ({
            todos: [
                ...state.todos,
                {
                    id: Date.now(),
                    text,
                    completed: false,
                },
            ],
        })),
    toggleTodo: (id) =>
        set((state) => ({
            todos: state.todos.map((todo) =>
                todo.id === id ? { ...todo, completed: !todo.completed } : todo
            ),
        })),
    deleteTodo: (id) =>
        set((state) => ({
            todos: state.todos.filter((todo) => todo.id !== id),
        })),
}));

export default useStore;


//显示组件
const DisplayTodos = () => {
    const { todos, deleteTodo } = useStore((state) => {
        return { todos: state.todos, deleteTodo: state.deleteTodo };
    });

    return (
        <ul>
            {todos.map((todo) => (
                <li
                    key={todo.id}
                    style={{
                        textDecoration: todo.completed
                            ? "line-through"
                            : "none",
                    }}
                    onClick={() => toggleTodo(todo.id)}
                >
                    {todo.text}
                    <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                </li>
            ))}
        </ul>
    );
};

export default DisplayTodos;


//添加todos表单
const TodosControl = () => {
    const addTodo = useStore((state) => state.addTodo);
    const [text, setText] = useState("");

    function handleSubmit(e) {
        e.preventDefault();
        addTodo(text);
        setText("");
    }

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
            />
            <button type="submit">Add</button>
        </form>
    );
};

export default TodosControl;


//todos
const App = () => {
    return (
        <>
            <DisplayTodos />
            <TodosControl />
        </>
    );
};

export default App;

新增代码案例

Case 1

简单使用

import create from "zustand";

export const useZustand = create((set) => ({
  counter: 0,
  buttonTitle: "Zustand : ",

  increment: () =>
    set((state) => {
      let increasingValue = state.counter + 1;
      state.counter >= 10 && (increasingValue = 10);
      return { counter: increasingValue };
    }),

  decrement: () =>
    set((state) => {
      let decreasingValue = state.counter - 1;
      state.counter <= 0 && (decreasingValue = 0);
      return { counter: decreasingValue };
    }),

  reset: () => set({ counter: 0 }),
}));

import { useZustand } from "../stores/zustandStore";

const CounterButtonZustand = () => {
  //取出使用 
  const buttonTitle = useZustand((state) => state.buttonTitle);
  const counter = useZustand((state) => state.counter);
  const handleIncrement = useZustand((state) => state.increment);
  const handleDecrement = useZustand((state) => state.decrement);

  useEffect(() => {
    console.log("zustand: " + counter);
  }, [counter]);

  return (
    <div className="counter">
      <h2 className="counter-title">{buttonTitle}</h2>

      <div className="counter-value-container">
        <button onClick={handleDecrement}>
          <FontAwesomeIcon icon={faMinus} size="4x" className="icons" />
        </button>

        <DisplayCounter value={counter} />

        <button onClick={handleIncrement}>
          <FontAwesomeIcon icon={faPlus} size="4x" className="icons" />
        </button>
      </div>
    </div>
  );
};

Case2

区分不同场景的使用

import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'

//暗黑模式切换 适用于某种全局的状态管理
let settingsStore = (set) => ({
  dark: false,
  toggleDarkMode: () => set((state) => ({ dark: !state.dark })),
})

let peopleStore = (set) => ({
  people: ['John Doe', 'Jane Doe'],
  addPerson: (person) =>
    set((state) => ({ people: [...state.people, person] })),
})

settingsStore = devtools(settingsStore)
//持久化
settingsStore = persist(settingsStore, { name: 'user_settings' })

//不需要持久化
peopleStore = devtools(peopleStore)

export const useSettingsStore = create(settingsStore)
export const usePeopleStore = create(peopleStore)



function App() {
  const toggleDarkMode = useSettingsStore((state) => state.toggleDarkMode)
  const dark = useSettingsStore((state) => state.dark)

  useEffect(() => {
    if (dark) {
      document.querySelector('body').classList.add('dark')
    } else {
      document.querySelector('body').classList.remove('dark')
    }
  }, [dark])

  return (
    <div className="App">
      <button onClick={toggleDarkMode}>Toggle Dark Mode</button>
      <p>People</p>
      <Input />
      <People />
    </div>
  )
}


Case3 装饰器模式

高度封装

useUsersStore.js

import create from "zustand"

const initialState = {
  users: [],
  loading: false,
  error: "",
}

const useUsersStore = create((set) => ({
  users: initialState.users,
  loading: initialState.loading,
  error: initialState.error,

  fetchUsers: async () => {
    set((state) => ({ ...state, loading: true }))
    try {
      const res = await fetch("https://jsonplaceholder.typicode.com/users")
      const users = await res.json()
      set((state) => ({ ...state, error: "", users }))
    } catch (error) {
      set((state) => ({
        ...state,
        error: error.message,
      }))
    } finally {
      set((state) => ({
        ...state,
        loading: false,
      }))
    }
  },

  // In our example we only need to fetch the users, but you'd probably want to define other methods here
  addUser: async (user) => {},
  updateUser: async (user) => {},
  deleteUser: async (id) => {},
}))

export default useUsersStore

useUsersFacade.js

import useUsersStore from "../store/useUsersStore"

import shallow from "zustand/shallow"

const useUsersFacade = () => {
  const { users, loading, error, fetchUsers } = useUsersStore(
    (state) => ({
      users: state.users,
      loading: state.loading,
      error: state.error,
      fetchUsers: state.fetchUsers,
    }),
    shallow
  )

  return { users, loading, error, fetchUsers }
}

export default useUsersFacade

Users.js

import { useEffect } from "react"

import useUsersFacade from "../facades/useUsersFacade"

const Users = () => {
  const { users, loading, error, fetchUsers } = useUsersFacade()

  useEffect(() => fetchUsers(), [])

  return (
    <div>
      {loading && <p data-testid="loading">Loading...</p>}
      {error && <p data-testid="error">{error}</p>}
      {users?.length > 0 && (
        <ul data-testid="users-list">
          {users.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

export default Users



import Users from "./pages/Users"
function App() {
  return <Users />
}
export default App

Case4

增删改经典案例

import {create} from 'zustand';
import {devtools, persist} from'zustand/middleware';


const courseStore = (set) => ({
    courses: [],
    addCourse: (course) => {
        set((state) => ({
            courses: [...state.courses, course]
        }))
    },
    removeCourse: (id) => {
        set((state) => ({
            courses: state.courses.filter((c) => c.id !== id)
        }))
    },
    toggleCourseState: (id) => {
        set((state) => ({
            courses: state.courses.map((c) => c.id === id ? {...c, completed: !c.completed} : c)
        }))
    }

})

const useCourseStore = create(devtools(
    persist(courseStore, {
        name:'courses'
    })
))

export default useCourseStore;

CourseForm.jsx

import React, { useState } from 'react';
import useCourseStore from '../app/courseStore';

const CourseForm = () => {
    const courses = useCourseStore((state) => state.courses)
    const addCourse = useCourseStore((state) => state.addCourse);
    const [courseTitle, setCourseTitle] = useState('')


    const handleFormSubmit = () => {
        if(courseTitle) {
            addCourse({
                id: courses.length + 1,
                title: courseTitle,
                completed:false
            });
            setCourseTitle('');
        }
    }


  return (
    <div className="form-container">
        <input value={courseTitle} onChange={(e) => setCourseTitle(e.target.value)} type="text" className="form-input" />
        <button onClick={() => handleFormSubmit()} className="form-submit-btn">Add Course</button>
    </div>
  )
}

export default CourseForm

CourseList.jsx

import React from 'react'
import useCourseStore from '../app/courseStore'

const CourseList = () => {
  const {courses, removeCourse, toggleCourseState} = useCourseStore((state) => state);
  return (
    <>
        <ul>
            {courses.map((course,index) => (
                <li key={index} className='course-item' style={{backgroundColor: course.completed ? '#b1f59d' : 'white' }}>
                    <span className="course-item-col-1">
                        <input type="checkbox" checked={course.completed} onChange={() => toggleCourseState(course.id)}/>
                    </span>
                    <span style={{color:'black'}}>{course?.title}</span>
                    <button className='delete-btn' onClick={(e) => removeCourse(course.id)}>Delete</button>
                </li>
            ))}
        </ul>
    </>
  )
}

export default CourseList

Case5

import create from 'zustand';

const actions = (set) => ({
  increase: () => set(state => ({ 
    count: state.count + 1 
  })),
  clear: () => set({ 
    count: 0 
  }),
});

const useStore = create(set => ({
  count: 0,
  ...actions(set),
}));

export default useStore;

//提前导出
export const selectCount = (state) => (
  state.count
);

export const selectIncrease = (state) => (
  state.increase
);

export const selectClear = (state) => (
  state.clear
);



import React from "react";
import useStore from '../store/store';
import { selectIncrease, selectClear } from '../store/selectors';

function Buttons() {
  //类似于这种 提前导出的话就直接写到括号里 调用不用写箭头函数 但是写哪里不是写 反正跑不了的都要写的 直接获取全部的话貌似对性能有所损耗
   //const toggleDarkMode = useSettingsStore((state) => state.toggleDarkMode)


  const increase = useStore(selectIncrease);
  const clear = useStore(selectClear);

  return (
    <React.Fragment>
      <button onClick={() => increase()}>Increase</button>
      <button onClick={() => clear()}>Reset</button>
    </React.Fragment>
  );
}

export default Buttons;

Case6

可变的/可修改的 使用immmer
store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        draft.kdramas.splice(dramaIndex, 1);
      })
    ),
  patchDrama: (payload) =>
    set(
      produce((draft) => {
        const drama = draft.kdramas.find((el) => el.id === payload.id);
        drama.name = payload.name;
      })
    ),
}));
import React, { useState } from "react";

import { useStore } from "./store";

export default function App() {
  const { kdramas, addDrama, removeDrama, patchDrama } = useStore();
  const [input, setInput] = useState("");
  const [isEdit, setIsEdit] = useState(false);
  const [update, setUpdate] = useState({
    id: null,
    name: "",
  });
  const addDramaHandler = (e) => {
    e.preventDefault();
    addDrama(input);
    setInput("");
  };
  const updateClickHandler = (drama) => {
    setIsEdit(true);
    setUpdate({
      id: drama.id,
      name: drama.name,
    });
  };
  const patchDramaHandler = (e) => {
    e.preventDefault();
    patchDrama(update);
    setUpdate({
      id: null,
      name: "",
    });
    setIsEdit(false);
  };
  return (
    <div>
      <div style={{ display: "flex" }}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add new Korean Drama"
        />
        <button onClick={addDramaHandler}>Add</button>
      </div>
      <br />
      {kdramas.map((el) => (
        <div key={el.id}>
          <h1>{el.name}</h1>
          <button onClick={() => removeDrama(el.id)}>
            <i className="ti ti-trash"></i>
          </button>{" "}
          <button onClick={() => updateClickHandler(el)}>
            <i className="ti ti-edit-circle"></i>
          </button>
        </div>
      ))}
      <br />
      {isEdit && (
        <div style={{ display: "flex", flexDirection: "column" }}>
          <label>Patch Korean Drama</label>
          <div style={{ display: "flex" }}>
            <input
              value={update.name}
              onChange={(e) => setUpdate({ ...update, name: e.target.value })}
            />
            <button onClick={patchDramaHandler}>Patch</button>
          </div>
        </div>
      )}
    </div>
  );
}

官网 https://zustand-demo.pmnd.rs/

github https://github.com/pmndrs/zustand

翻译文档 https://zhuanlan.zhihu.com/p/475571377

相关文章介绍 https://juejin.cn/post/7038780686991376415

posted @ 2023-08-05 15:12  CoderWGB  阅读(364)  评论(0编辑  收藏  举报