Scott

如何使用RTK in nextjs 13 a year ago

react
8206个字符
共有272人围观

RTK简介

传统要想跑通redux需要借助很多第三方包,而且还要写很多重复性的代码。为了降低学习门槛,react推出了自己的工具包 - redux toolkit(简称RTK)。

RTK集成了很多第三方库, 让初学者可以开箱即用。

来看看RTK的核心api:

  • 1,configureStore(): 底层包裹的是createStore,把一个个小的reducer自动合并成一个大的reducer,可以添加任何中间件,原生支持redux-thunk和Redux DevTools Extension
  • 2,createReducer(): 以前操作reducer,需要频繁的去copy state, eg{...state.xxx}; 现在不用了,RTK集成了immer, 对任意对象的修改会自动返回一个新的state, 如现在修改completed属性可以直接这么写:state.todos[3].completed = true对state的操作不用写return语句
  • 3,createAction(type, prepareAction?): 用action type字符串自动生成一个action, eg: const increment = createAction<number>('counter/increment')
  • 4,createSlice(): 接收一个包含reducer functions,a string slice name, an initial state value的object,自动生成一个reducer和对应的action creators 及 action types.
  • 5,createAsyncThunk: 接收一个string的action type 和 一个返回Promise对象的function,基于这个Promise,createAsyncThunk会自动生成并派发pending/fulfilled/rejected这3种action type的thunk

如何使用RTK

安装

#react-redux提供Provider
npm i @reduxjs/toolkit react-redux -S

创建store

这里我选择在lib目录下创建store, 在storefeatures目录下创建reducer,以countSlice为例:

store/index.tsx

import { configureStore } from '@reduxjs/toolkit'
import countSlice from './features/countSlice'

export const makeStore = () => {
  return configureStore({
    reducer: {
      count: countSlice
    },
  })
}

// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

创建reducer

reducer统一放在store/features目录下:

features/counterSlice.tsx

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import userAPI from '@/lib/api/user'

// First, create the thunk
export const fetchSummary = createAsyncThunk(
  'users/getSummary',
  async () => {
    const response = await userAPI.getSummary()
    return response.data
  }
) 

export const incrementAsync = createAsyncThunk(
  "count/incrementAsync",
  async (amount:number)=>{
    await new Promise((resolve)=> setTimeout(resolve,2000))
    return amount
  }
)

export interface CounterState {
  value: number,
  fetchStatus:string;
  summary:{
    blog_all_count:number,
    blog_count_public:number;
    tag_count_all:number;
    visited:number;
  }
}

const initialState: CounterState = {
  value: 0,
  fetchStatus:'',
  summary: {
    blog_all_count:0,
    blog_count_public:0,
    tag_count_all:0,
    visited:0
  }
}

export const counterSlice = createSlice({
  name: 'counter', //will be used for prefix of actionCrators
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
  extraReducers(builder) {
    builder.addCase(fetchSummary.pending,(state, _)=>{
      state.fetchStatus = 'pending'
    })
    .addCase(fetchSummary.fulfilled,(state, action)=>{
      // console.log(action.payload) 
      // http response: action.payload
      state.fetchStatus = 'success'
      state.summary = action.payload.data
    }).addCase(fetchSummary.rejected,(state,_)=>{
      state.fetchStatus = 'rejected'
    })
    .addCase(incrementAsync.pending,()=>{
      console.log("ncremenAsync.pending")
    })
    .addCase(incrementAsync.fulfilled,(state,action:PayloadAction<number>)=>{
      state.value += action.payload
    })
  },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

创建Provider,并用Provider包裹根组件

根组件在哪: 对nextjs来说根组件就是最外层的layout.tsx

1, 首先我们来创建一个Provider

'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '@/lib/store'

export default function StoreProvider({
  children,
}: {
  children: React.ReactNode
}) {
  const storeRef = useRef<AppStore>()
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()
  }

  return <Provider store={storeRef.current}>{children}</Provider>
}

2, 用Provide包裹根组件

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import StoreProvider from './StoreProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <StoreProvider>
          {children}
        </StoreProvider>
      </body>
    </html>
  )
}

使用Reducer

"use client"
import { decrement, fetchSummary, increment, incrementAsync } from '@/lib/store/features/countSlice'
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks'
import React, { memo } from 'react'

const Component1 = memo(() => {
  const count = useAppSelector(state=>state.count.value)
  const dispatch = useAppDispatch()
  const summary = useAppSelector(state=>state.count.summary)
  
  return (
    <div className='flex flex-col border'>
        <div className='flex gap-2'>
            <button className='btn' onClick={()=>dispatch(increment())}>+1</button>
            <button className='btn' onClick={()=>dispatch(decrement())}>-1</button>
            <button className='btn' onClick={()=>dispatch(incrementAsync(10))}>异步+10</button>
        </div>
        <h3>{count}</h3>
        <div>
            <button className='btn' onClick={()=>dispatch(fetchSummary())}>发送http请求:</button>
            <ul>
                <li>blog_all_count:{summary.blog_all_count}</li>
                <li>blog_count_public:{summary.blog_count_public}</li>
                <li>tag_count_all:{summary.tag_count_all}</li>
                <li>visited:{summary.visited}</li>
            </ul>
        </div>
    </div>
  )
})

export default Component1

"use client"

import { AppDispatch, RootState } from '@/lib/store'
import { decrement, fetchSummary, increment, incrementAsync } from '@/lib/store/features/countSlice'
import React, { memo } from 'react'
import { useDispatch, useSelector } from 'react-redux'

const Component2 = memo(() => {
  const count = useSelector((state:RootState)=>state.count.value)
  const summary = useSelector((state:RootState)=>state.count.summary)
  const dispatch = useDispatch<AppDispatch>()
  return (
    <div className='flex flex-col border'>
        <div className='flex gap-2'>
            <button className='btn' onClick={()=>dispatch(increment())}>+1</button>
            <button className='btn' onClick={()=>dispatch(decrement())}>-1</button>
            <button className='btn' onClick={()=>dispatch(incrementAsync(10))}>异步+10</button>
        </div>
        <h3>{count}</h3>
        <div>
            <button className='btn' onClick={()=>dispatch(fetchSummary())}>发送http请求:</button>
            <ul>
                <li>blog_all_count:{summary.blog_all_count}</li>
                <li>blog_count_public:{summary.blog_count_public}</li>
                <li>tag_count_all:{summary.tag_count_all}</li>
                <li>visited:{summary.visited}</li>
            </ul>
        </div>
    </div>
  )
})

export default Component2

可以看到,除了react-redux,RTK不用额外使用第三方库,就能使用rudux。如果你怀念老项目的话,也可以使用react-redux来连接store, 写法还是和以前一样(connect(mapStateToProps,mapDispatchToPorps)(Componet)), 需要注意的RTK操作是客户端行为

完整demo见 rtk_nextjs

建议使用rtk command来创建store