五分钟教你使用React核心api去实现一个全局状态管理器<<附仓库地址>>

1,137 阅读5分钟

1. 前言

我们在开发项目的过程中往往会遇到一些很复杂的应用场景,复杂的场景必然避免不了多个组件的嵌套使用,这时对于数据的传递大多数人会去一层一层的传递数据,其实我们这样做是有隐患的,props传递层级过深,如果出现bug需逐层排查,而且代码不够优秀,不利于后人理解,并且维护成本高

!!!如果你已经足够了解context和useReducer如何使用,请直接阅读双剑合璧

通常我们的解决办法一般是用context或者使用一些状态管理器(redux,mobx,recoil...),但是有一点我们需要知道,并不是所有的项目都适合用redux,有时redux使用不当无疑是增加了我们的开发成本

什么时候使用redux?

  • 组件需要根据状态发生显示变化
  • state并不总是以单向的方式线性流动
  • 存在组件需要更新全局状态
  • 存在组件需要更新另一个组件的状态
  • 存在状态以许多不同的方式更新
  • 状态树结构复杂
  • 某个状态需要在全局使用或共享(例如角色权限等信息)

2. context的使用

20211026163726.jpg

我们新建一个名为Context的页面,在里面嵌套一个层级很深的组件(一般开发中很常见),去实现一个跨级传参和兄弟组件传参

  • Context 页面
  • ParentComOne 父组件1
  • ParentComTwo 父组件2
  • SonCom 子组件

使用props传递的依赖图:

Untitled-2021-10-18-1154.png

代码:

import React, {useState, useImperativeHandle, useRef, forwardRef} from 'react';

//页面
const Context = () => {
    const [count, setCount] = useState(0);
    const ref = useRef();  //定义引用用来获取ParentComOne向外暴露的方法
    return (
        <div>
            <h1>context</h1>
            {/*传递count和setCount*/}
            <ParentComOne ref={ref} count={count} setCount={setCount}/>
            {/*获取到setNow方法后传递给ParentComTwo组件*/}
            <ParentComTwo setNow={ref.current?.setNow} />
        </div>
    );
};


//父组件1
const ParentComOne = forwardRef((props, ref) => {
    const [now, setNow] = useState(Date.now());
    //使用useImperativeHandle将setNow方法暴露
    useImperativeHandle(ref, () => {
        return {
            setNow
        }
    })

    return <div>
        <h3>ParentComOne----{now}</h3>
        {/*传递count和setCount*/}
        <SonCom {...props}/>
    </div>
})

//父组件2
const ParentComTwo = ({setNow}) => {
    //调用ParentComOne的setNow方法
    return <div onClick={() => setNow(Date.now())}>
        <h3>ParentComTwo</h3>
    </div>
}

//子组件
const SonCom = (props) => {
    const {count, setCount} = props;
    // 调用组件组件传递的值和方法去更改视图
    return <div onClick={() => setCount(count + 1)}>
        <h3>SonCom---{count}</h3>
    </div>
}


export default Context;

效果如下:

Oct-26-2021 14-52-57.gif

这种方法虽然可以实现我们想要的效果,但是文章开头有提到过,它并不是一个好的方案

小试牛刀

使用createContext和useContext实现的依赖图:

Untitled-2021-10-18-1154.png 代码:

import React, {useState, createContext, useContext} from 'react';

const TestContext = createContext({});

//页面
const Context = () => {
    const [count, setCount] = useState(0);
    const [now, setNow] = useState(Date.now());

    return (
        <div>
            <h1>context</h1>
            <TestContext.Provider value={{count, setCount, now, setNow}}>
                <ParentComOne/>
                <ParentComTwo/>
            </TestContext.Provider>
        </div>
    );
};


//父组件1
const ParentComOne = () => {
    const {now} = useContext(TestContext);
    return <div>
        <h3>ParentComOne----{now}</h3>
        <SonCom/>
    </div>
}

//父组件2
const ParentComTwo = () => {
    const {setNow} = useContext(TestContext);
    return <div onClick={() => setNow(Date.now())}>
        <h3>ParentComTwo</h3>
    </div>
}

//子组件
const SonCom = () => {
    const {count, setCount} = useContext(TestContext);
    return <div onClick={() => setCount(count + 1)}>
        <h3>SonCom---{count}</h3>
    </div>
}


export default Context;

效果如下,发现同样可以实现,我们集中管理各个组件状态,并且代码通俗易懂,依赖关系不复杂

Oct-26-2021 15-06-36.gif

3. useReducer的使用

20211026151620.jpg

import React, {useReducer} from 'react';

const initialState = {count: 0, now: Date.now()};

const reducer = (state, action) => {
    switch (action.type) {
        case 'now':
            state.now = Date.now();
            return {...state};
        case 'add':
            state.count++;
            return {...state};
        default :
            return state;
    }
};

const Reducer = () => {
    const [state, dispath] = useReducer(reducer,initialState)
    return (
        <div>
            <h1>reducer</h1>
            <h3 onClick={() => dispath({type: 'now'})}>{state.now}</h3>
            <h3 onClick={() => dispath({type: 'add'})}>{state.count}</h3>
        </div>
    );
};

export default Reducer;

useReducer的使用很简单,它可以使我们的状态与视图分离,让代码看上去更直观

效果如下:

Oct-26-2021 15-20-04.gif

4. 让大脑飞一会

我们明白context和reducer的特性以后,想办法把它们关联到一起就能去模拟redux啦,其实原理非常简单,有想法的小伙伴可以暂停阅读,先去项目里试试,如果想不到再回来继续看也不迟~

第一步: 初始化自己的state和reducer

第二部: 使用createContext创建context

第三部: 在最外层组件使用useReducer去创建自己的reducer,然后使用context.Provider去包裹需要进行状态管理的组件,最后将useReducer返回的state和dispatch传给context

第四步: 在组件内使用useContext去获取state和dispatch方法并使用

5. 双剑合璧

好了,那有了实现方案以后我们一步一步的去进行,为了演示效果更直观一些,我们将会使用reducer去管理一个列表数据,它可以进行增加数据和删除数据

在根目录下新建store以及store/index.js

import {createContext} from 'react';

export const initialState = {
    //列表数据
    list: [...Array.from({length: 3}).keys()]
};

const actionFunc = {
    //增加一项
    add(state, {value, callback}) {
        //判断如果无内容给提示并return
        if (value.trim() === '') {
            alert('请输入内容')
            return;
        }
        //push一条
        state.list.push(value);
        //添加成功的回调函数
        callback();
    },
    //删除某一项
    del(state, id) {
        state.list = state.list.filter(item => item !== id);
    }
}


export const reducer = (state, {type, payload}) => {
    //浅拷贝数据
    const newState = {...state};
    //actionFunc对有应方法才去执行对应方法
    actionFunc[type] && actionFunc[type](newState, payload);
    return newState;
};

export const StoreContext = createContext();

修改App.js

import {HashRouter, Switch, Route, Redirect} from 'react-router-dom';
import ContextPage from './pages/context/Context'
import ReducerPage from './pages/reducer/Reducer'
import Test from "./pages/test/Test";
//引入我们刚刚定义的state、reducer、context
import {StoreContext, initialState, reducer} from './store';
import {useReducer} from 'react';


function App() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <HashRouter>
            <Switch>
                {/*!!!将state和dispatch传递给context,这样它下面的所有组件都可以去使用!!!*/}
                <StoreContext.Provider value={{state, dispatch}}>
                    <Route path={'/context'}><ContextPage/></Route>
                    <Route path={'/reducer'}><ReducerPage/></Route>
                    <Route path={'/test'}><Test/></Route>
                </StoreContext.Provider>
            </Switch>
        </HashRouter>
    );
}

export default App;

然后新建test页面

import React, {useContext, useRef} from 'react';
import {StoreContext} from '../../store';
import {Link} from 'react-router-dom';

const delStyles = {
    paddingLeft: 20,
    cursor: 'pointer'
}
const liStyles = {
    margin: '5px 0px'
}

const Test = () => {
    const {state: {list}, dispatch} = useContext(StoreContext);
    const inputRef = useRef();
    return (
        <div>
            <h1>test</h1>
            <Link to={'/reducer'}>去reducer页面</Link><br/>
            <input ref={inputRef} type="text"/>
            <button onClick={() => {
                dispatch({
                    type: 'add',
                    payload: {
                        value: inputRef.current?.value,
                        callback: () => {
                            inputRef.current.value = '';
                        }
                    }
                })
            }}>添加
            </button>
            <ul>
                {
                    list.map(item => {
                        return <li key={item} style={liStyles}>
                            <span>{item}</span>
                            <span
                                style={delStyles}
                                onClick={() => {
                                    dispatch({
                                        type: 'del',
                                        payload: item
                                    })
                                }
                                }>删除</span>
                        </li>
                    })
                }
            </ul>
        </div>
    );
};

export default Test;

效果如下:

Oct-26-2021 16-29-11.gif

6. 结尾

到这里我们最终的效果就实现了,原理是不是很简单呢

它就这几行代码,真的能实现redux所有的功能吗?

这种方式会不会性能不太好啊?

如果同学们有上面的疑问,我暂时先不回答哈,等有时间我会在出一篇redux源码解析的文章,有疑问的同学记得来捧场哦~

最后的最后附上git仓库地址

OSZAR »