react-router + react-redux +hooks

279 阅读4分钟

demo需求

首页是登录页,路由是登录页和注册页和忘记密码页面,3页面共享redux的数据,3页面通过路由(react-router-config)切换。

首页重定向到登录页: image.png

拿到全局数据TOM image.png

注册页:拿到全局数据TOM

image.png

文件结构

image.png

在routes里面写路由文件

import React from 'react';
import { Redirect } from 'react-router-dom';
import Frame from './pages/login/Frame';
import Sign from './pages/login/Sign';
import Forget from './pages/login/Forget';
import Login from './pages/login/Login';

export const Routes = [
  {
    path: '/login',
    component: Frame,
    exact: false,
    routes: [
      {
        path: '/login',
        exact: true,
        component: Login,
      },
      {
        path: '/login/sign',
        exact: true,
        component: Sign,
      },
      {
        path: '/login/forget',
        exact: true,
        component: Forget,
      },
    ],
  },
  {
    path: '*',
    render: () => <Redirect to="/login"></Redirect>,
  },
];

react-router-config使用

先安装react-router-domreact-router-config yarn add react-router-dom react-router-config import { renderRoutes } from 'react-router-config';

使用场景:

react-router-config用于静态路由配置,属于react-router的一个插件,主要用于集中管理路由配置

在src\pages\login\Frame.jsx这里是嵌套路由触发的组件,引入react-router-config

import React from 'react';
import { renderRoutes } from 'react-router-config';

const Frame = ({route}) => {//写法2 const Frame = (props) 
  return (
    <div>
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          backgroundColor: 'gray',
          height: 100,
          padding: 25,
          boxSizing: 'border-box'
        }}>
        <div>logo</div>
        <div>顶部导航栏</div>
        <div>用户登录</div>
      </div>


      {/* 上面和下面是页头和页脚是固定的。这里负责渲染子路由的页面 */}
      <div>{renderRoutes(route.routes)}</div> //写法2 props.route.routes


      <div
        style={{
          position: 'fixed',
          bottom: 0,
          textAlign: 'center',
          height: 100,
          paddingTop: 30,
          boxSizing: 'border-box',
          backgroundColor: 'grey',
          width: '100%'
        }}>
        页脚footer企业信息
      </div>
    </div >
  );
}

export default Frame;

Forget.jsx

import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { GlobalContext } from '../../store';

const Forget = () => {
  const history = useHistory()
  const { state } = useContext(GlobalContext)

  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div>
        <h1>忘记密码页面</h1>
        <div>
          账号 <input type="text" />
        </div>
        <div>
          密码 <input type="text" />
        </div>
        <div>
          <button
            onClick={() => {
              history.goBack()
            }}
          >返回</button>
        </div>
        <h2>
          {
            state.username && (
              <div>
                从全局拿到登录用户名 {state.username}
              </div>
            )
          }
        </h2>
      </div>
    </div>
  );
}

export default Forget;

Frame.jsx

import React from 'react';
import { renderRoutes } from 'react-router-config';

const Frame = ({route}) => {
  return (
    <div>
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          backgroundColor: 'gray',
          height: 100,
          padding: 25,
          boxSizing: 'border-box'
        }}>
        <div>logo</div>
        <div>顶部导航栏</div>
        <div>用户登录</div>
      </div>


      {/* 上面和下面是页头和页脚是固定的。这里负责渲染子路由的页面 */}
      <div>{renderRoutes(route.routes)}</div>


      <div
        style={{
          position: 'fixed',
          bottom: 0,
          textAlign: 'center',
          height: 100,
          paddingTop: 30,
          boxSizing: 'border-box',
          backgroundColor: 'grey',
          width: '100%'
        }}>
        页脚footer企业信息
      </div>
    </div >
  );
}

export default Frame;

Login.jsx

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

const Login = () => {
  const { state, dispatch } = useContext(GlobalContext)
  
  const [username, setname] = useState('')
  const [password, setpwd] = useState('')

  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div>
        <h1>登录页面</h1>
        <div>
          账号 <input type="text" value={username} onInput={e => setname(e.target.value)} />
        </div>
        <div>
          密码 <input type="text" value={password} onInput={e => setpwd(e.target.value)} />
        </div>
        <div>
          <button
            onClick={() => {
              dispatch({
                type: 'login',
                username,
                password
              })
            }}
          >登录</button>
        </div>
        <div>
          <Link to="/login/sign">注册</Link>
          <Link to="/login/forget" style={{ marginLeft: 20 }}>忘记密码</Link>
        </div>


        <h2>
          {
            state.username && (
              <div>
                登录成功, 欢迎全局状态来的 {state.username}
              </div>
            )
          }
        </h2>
      </div>
    </div>
  );
}

export default Login;

Sign.jsx

import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { GlobalContext } from '../../store';


const Sign = () => {
  const history = useHistory()
  const { state } = useContext(GlobalContext)


  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div>
        <h1>注册页面</h1>
        <div>
          账号 <input type="text" />
        </div>
        <div>
          密码 <input type="text" />
        </div>
        <div>
          <button
            onClick={() => {
              history.goBack()
            }}
          >返回</button>

          <h2>
            {
              state.username && (
                <div>
                  从全局拿到登录用户名 {state.username}
                </div>
              )
            }
          </h2>
        </div>
      </div>
    </div>
  );
}

export default Sign;

APP.js

import React, { useReducer } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Routes } from './routes';
import { GlobalContext, initState, reducer } from './store';

function App() {
  const [state, dispatch] = useReducer(reducer, initState)
  return (
    <GlobalContext.Provider value={{ state, dispatch }}>
      <BrowserRouter>
        {renderRoutes(Routes)}
      </BrowserRouter>
    </GlobalContext.Provider>
  );
}

export default App;

routes

import React from 'react';
import { Redirect } from 'react-router-dom';
import Frame from './pages/login/Frame';
import Sign from './pages/login/Sign';
import Forget from './pages/login/Forget';
import Login from './pages/login/Login';

export const Routes = [
  {
    path: '/login',
    component: Frame,
    exact: false,
    routes: [
      {
        path: '/login',
        exact: true,
        component: Login,
      },
      {
        path: '/login/sign',
        exact: true,
        component: Sign,
      },
      {
        path: '/login/forget',
        exact: true,
        component: Forget,
      },
    ],
  },
  {
    path: '*',
    render: () => <Redirect to="/login"></Redirect>,
  },
];

stoe.js

import { createContext } from 'react';
export const GlobalContext = createContext();

export const initState = {
  username: '',
  password: ''
}

export function reducer(state, actions) {
  const {username,password} = actions
  if (actions.type === 'login') {
    return {
      ...state,
      username,
      password,
    }
  }

  return state
}

HOOKS延伸

React.createContext()

为什么出现这个api? 简单说,就是如果context的值有更新时,没办法保证所有子节点一定能更新。
为什么?因为在老的Context中由上而下的“触发链”有可能被shouldComponentUpdate打断。

原理也很简单,本质就是要实现跨过中间components的通信,这里用pub-sub模式实现一个简单的createContext

const emitter = {
  listeners: [],
  on: fn => {
    emitter.listeners.push(fn);
  },
  off: fn => {
    emitter.listeners.splice(emitter.listener.findIndex(fn), 1);
  },
  emit: value => {
    emitter.listeners.forEach(fn => fn(value));
  }
};

function createContext(defaultValue) {
  class Provider extends React.PureComponent {
    componentDidUpdate() {
      emitter.emit(this.props.value);
    }

    componentDidMount() {
      emitter.emit(this.props.value);
    }

    render() {
      return this.props.children;
    }
  }

  class Consumer extends React.PureComponent {
    constructor(props) {
      super(props);
      this.state = { value: defaultValue };

      emitter.on(value => {
        console.log(value);
        this.setState({ value });
      });
    }

    render() {
      return this.props.children(this.state.value);
    }
  }

  return { Provider, Consumer };
}

解释一下:
1. 创建了一个emitter
2.Provider里的`componentDidUpdate``componentDidMount`中触发`emmiter.emit`
3.Consumer里注册监听,一旦有value变化,便触发`this.setState`,自然会触发re-render

所以这里我们能看出来,只要`Provider`的value有变化,就一定会触发`Consumer`的state变化。在老的`Context`中被`shouldComponentUpdate`打断的“触发链”又被重新接上了。

使用 useReducer 减少 Context 的复杂程度

直接使用父组件 state 带来的性能问题

注意看上面的动图,在点击子组件的 【number + step】 按钮的时候,虽然 count 的值没有发生任何变化,但是一直触发 re-render,即使子组件是通过 React.memo 包装过的。

出现这个问题原因是 React.memo 只会对 props 进行浅比较,而通过 Context 我们直接将 state 注入到了组件内部,因此 state 的变化必然会触发 re-render,整个 state 变化是绕过了 memo。

使用 useMemo() 解决 state Context 透传的性能问题

既然 React.memo() 无法拦截注入到 Context 的 state 的变化,那就需要我们在组件内部进行更细粒度的性能优化,这个时候可以使用 useMemo()

1、使用 useMemo 优化子组件渲染

下面是对子组件的改造,去掉了 React.memo,在 return 内部通过 useMemo() 包装,并且声明了所有依赖项:(包括:step/number/count/dispatch)

import React, { useContext, useMemo } from 'react';

import { MyContext } from './context-manager';

export default (props = {}) => {
    const { state, dispatch } = useContext(MyContext);
    return useMemo(() => {
        console.log('[Child] RE-RENDER');
        return (
            <div>
                <p>step is : {state.step}</p>
                <p>number is : {state.number}</p>
                <p>count is : {state.count}</p>
                <hr />
                <div>
                    <button onClick={() => { dispatch({ type: 'stepInc' }) }}>step ++</button>
                    <button onClick={() => { dispatch({ type: 'numberInc' }) }}>number ++</button>
                    <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button>
                </div>
            </div>
        )
    }, [state.count, state.number, state.step, dispatch]);
}


react-router-config参考

createContex

官方React.createContext