标签 react 下的文章

React Hooks 在 2018 年年底就已经公布了,正式发布是在 2019 年 5 月,关于它到底能做什么用,并不在本文的探讨范围之内,本文旨在摸索,如何基于 Hooks 以及 Context,实现多组件的状态共享,完成一个精简版的 Redux。

初始化一个 React 项目

yarn create create-app hooks-context-based-state-management-react-app
cd hooks-context-based-state-management-react-app
yarn start

或者可以直接 clone 本文完成的项目:

git clone https://github.com/pantao/hooks-context-based-state-management-react-app.git

设置我们的 state

绝大多数情况下,我们其实只需要共享会话状态即可,在本文的示例中,我们也就只共享这个,在 src 目录下,创建一个 store/types.js 文件,它定义我们的 action 类型:

// 设置 session
const SET_SESSION = "SET_TOKEN";
// 销毁会话
const DESTROY_SESSION = "DESTROY_SESSION";

export { SET_SESSION, DESTROY_SESSION };

export default { SET_SESSION, DESTROY_SESSION };

接着定义我们的 src/reducers.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

const initialState = {
  // 会话信息
  session: {
    // J.W.T Token
    token: "",
    // 用户信息
    user: null,
    // 过期时间
    expireTime: null
  }
};

const reducer = (state = initialState, action) => {
  console.log({ oldState: state, ...action });

  const { type, payload } = action;
  switch (type) {
    case SET_SESSION:
      return {
        ...state,
        session: {
          ...state.session,
          ...payload
        }
      };
    case DESTROY_SESSION:
      return {
        ...state,
        session: { ...initialState }
      };
    default:
      throw new Error("Unexpected action");
  }
};

export { initialState, reducer };

创建 src/actions.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

export const useActions = (state, dispatch) => {
  return {
    login: async (username, password) => {
      console.log(`login with ${username} & ${password}`);
      const session = await new Promise(resolve => {
        // 模拟接口请求费事 1 秒
        setTimeout(
          () =>
            resolve({
              token: "J.W.T",
              expireTime: new Date("2030-09-09"),
              user: {
                username,
                password
              }
            }),
          1000
        );
      });

      // dispatch SET_TOKEN
      dispatch({
        type: SET_SESSION,
        payload: session
      });

      return session;
    },
    logout: () => {
      dispatch({
        type: DESTROY_SESSION
      });
    }
  };
};

关键时刻,创建 store/StoreContext.js

import React, { createContext, useReducer, useEffect } from "react";
import { reducer, initialState } from "./reducers";
import { useActions } from "./actions";

const StoreContext = createContext(initialState);

function StoreProvider({ children }) {
  // 设置 reducer,得到 `dispatch` 方法以及 `state`
  const [state, dispatch] = useReducer(reducer, initialState);

  // 生成 `actions` 对象
  const actions = useActions(state, dispatch);

  // 打印出新的 `state`
  useEffect(() => {
    console.log({ newState: state });
  }, [state]);

  // 渲染 state, dispatch 以及 actions
  return (
    <StoreContext.Provider value={{ state, dispatch, actions }}>
      {children}
    </StoreContext.Provider>
  );
}

export { StoreContext, StoreProvider };

修改 src/index.js

打开 src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

做如下修改:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { StoreProvider } from "./context/StoreContext"; // 导入 StoreProvider 组件

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

src/App.js

内容如下:

import React, { useContext, useState } from "react";
import logo from "./logo.svg";
import "./App.css";

import { StoreContext } from "./store/StoreContext";
import { DESTROY_SESSION } from "./store/types";

function App() {
  const { state, dispatch, actions } = useContext(StoreContext);

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const { user, expireTime } = state.session;

  const login = async () => {
    if (!username) {
      return alert("请输入用户名");
    }
    if (!password) {
      return alert("请输入密码");
    }
    setLoading(true);
    try {
      await actions.login(username, password);
      setLoading(false);
      alert("登录成功");
    } catch (error) {
      setLoading(false);
      alert(`登录失败:${error.message}`);
    }
  };

  const logout = () => {
    dispatch({
      type: DESTROY_SESSION
    });
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {loading ? <div className="loading">登录中……</div> : null}
        {user ? (
          <div className="user">
            <div className="field">用户名:{user.username}</div>
            <div className="field">过期时间:{`${expireTime}`}</div>
            <div className="button" onClick={actions.logout}>
              使用 actions.logout 退出登录
            </div>
            <div className="button" onClick={logout}>
              使用 dispatch 退出登录
            </div>
          </div>
        ) : (
          <div className="form">
            <label className="field">
              用户名:
              <input
                value={username}
                onChange={e => setUsername(e.target.value)}
              />
            </label>
            <label className="field">
              密码:
              <input
                value={password}
                onChange={e => setPassword(e.target.value)}
                type="password"
              />
            </label>
            <div className="button" onClick={login}>
              登录
            </div>
          </div>
        )}
      </header>
    </div>
  );
}

export default App;

总结

整个实现我们使用到了 ReactuseContext 共享上下文关系,这个是关系、useEffect 用来实现 reducerloguseReducer 实现 redux 里面的 combineReducer 功能,整体上来讲,实现还是足够绝大多数中小型项目使用的。

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用:

react-native init MyApp --template=parcmg

初始化完成之后,按下面的方式执行命令:

cd MyApp
node setup.js
npm install
react-native link react-native-gesture-handler

完成之后,即可像往常一样开发了:

react-native run-ios

要开始构建你的第一个 React App,最简单的方法莫过于使用下面这两个 JSFiddle 示例了:

Create React App

Create React App 是一个新的受官方支持的用于创建 React 单页面应用的工具,它提供了一个一些无需任何配置那可拿来即用的现代化构建工具,需要 Node 4 或者更高版本的支持。

但是需要注意的是,它还是有一些使用上的限制,而且它也仅仅只适用于单页面应用,如果你更高的灵活性或者将 React 整合到现有的项目中,那你可能就需要下面这些其它的解决方案了。

Starter Pack

如果你才刚刚开始了解 React,那么下载 Starter kit 是另一个不错的选择, Starter kit 包含了预建的 React 以及 React Dom 示例复本。

下载 Starter kit 15.3.1

在 Starter kit 的根目录下,创建一个名为 helloworld.html 的文件,包含以下的内容:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="build/react.js"></script>
    <script src="build/react-dom.js"></script>
    <script src="https://unpkg.com/babel-core@5.8.38/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      ReactDOM.render(
        <h1>Hello, world!</h1>,
        document.getElementById('example')
      );
    </script>
  </body>
</html>

如上所示,这种在JavaScript 中包含 XML 语法的实现我们称之为 JSX,你可以查看 JSX 语法说明 以了解更多关于 JSX 的使用帮助,为了将其编译为浏览器可识别的 JavaScript 代码,我们使用了 <script type="text/babel">,此时 Babel 将直接在浏览器编译它,直接在浏览器中打开该页面,你就将看到应用已经执行了。

分开的文件

你的 React JSX 代码,还可以被分开存储在不同的文件中,创建一个 src/helloworld.js 文件:

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

然后在 helloworld.html 代码中引入该文件:

<script type="text/babel" src="src/helloworld.js"></script>
这里需要注意一点,有一些浏览器(比如 Chrome),可能只允许通过 HTTP 协议访问文件。

npm 或者 Bower 中使用 React

你同样还可以使用如 npm 或者 bower 这样的包管理工具, 这在后面的文章中会详细涉及到。