react

放弃 Next.js, 拥抱 react-router

Next.js 是一个好库,设计上很优雅,实现上也没有什么大的问题。然而,考虑再三我还是决定暂时移除 next.js 了。不是我不想要服务端渲染,而是整个 JS 的生态圈大部分的库都没有考虑服务端渲染,这就导致我在学习和使用的过程中时不时要自己考虑如何处理服务端渲染的情形。本身我就是个初学者,连教程都看不太懂,再考虑服务端渲染,就一个头两个大了。另外一个原因就是组里另一个项目使用了 react-router, 没必要两个都搞了。这里姑且记录下移除 next.js, 添加 react-router 的过程,以便以后参考。

删除 nextjs

yarn remove next

更改 package.json scripts 部分的脚本:

"scripts": {
    "start": "react-scripts start",
    "dev": "react-scripts dev",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

还好之前 create-react-app 创建的 index.js 和 App.js 还没删掉,直接就能用了。

样式

nextjs 中规定了只能用 module css 或者 scoped css, 而在 react 中没有硬性的规定。如果使用原生 CSS 的话自然最简单,但是容易名字冲突。鉴于另一个项目使用了 sass, 这里也用 sass 以统一下开发体验。

页面

暂时先保留 pages, components, layouts 三个文件夹,但是需要使用 react-router 路由。

yarn add react-router react-router-dom

在 index.js 中使用 router, 去掉 <App/>

Link

更改所有的 Link. 从 import Link from 'next/link' 改成 import {Link} from 'react-router-dom', 其中需要把 href 改为 to.

更改所有的 useRouter 的跳转,需要使用 useHistory.

获取数据

页面里的 getServerSideProps 显然是不能用了,需要改用 redux 的 thunk 来获取数据,所以需要以下几步:

  1. 设置对后端 API 的代理,在 package.json 中添加 "proxy": "http://localhost:4000", 即可
  2. 引入 redux, 设计 store 等
  3. 调整请求接口到 redux 中

一般来说,我们把相应的 getServerSideProps 函数的逻辑转移到对应的 Page 组件的 useEffect(fn, []) 钩子中就可以了。

代理的问题

在 next.js 中,需要在两个地方指定代理,一个是后端在 server 预加载数据的时候,需要指定上游 API 的地址,另一方面,在浏览器中发送 ajax 请求的时候需要设定代理访问上游 API, 否则会有跨域的问题。

使用 react-router 之后问题就简单了,所有数据都是从前端加载的,所以只需要指定代理的地址就好了。但是也要考虑到几种不同的环境:

  1. 开发阶段的配置
  2. 部署阶段的配置
  3. 如果有多个后端如何处理
  4. 用户鉴权放在哪里

综合考虑后,采用以下几点:

  1. 用户鉴权放在前端的 server, 也就是 express 中,这样就避免了上游 API 再添加复杂的逻辑,但是用户列表可以放在后端中。
  2. 开发环境和部署环境统一使用 express 代理多个后端,这样在开发环境也能保证和生产环境一样的效果,方便 debug.

参考

  1. https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually
  2. https://stackoverflow.com/questions/50260684/bundle-react-express-app-for-production
  3. https://dev.to/nburgess/creating-a-react-app-with-react-router-and-an-express-backend-33l3

Nextjs 中遇到的一些坑

nextjs 的 Link 无法自定义 escape

nextjs 中的 Link 的 href 对象如果传的是字典,直接调用的是 nodejs 的 URL 库,不能自定义 escape, 比如说空格会被强制格式化成加好,而不是 %20. 而且好像它使用的这个 API 在 11.0 已经 deprecated 了,所以需要啥 url 的话,还是自己格式化吧~

不支持 loading spinner

Nextjs 不支持在页面跳转的时候触发 Loading Spinner, 也就是转动的小圆圈,所以需要自己实现一下,可以用 nprogress

在 _app.js 中:

import Router from 'next/router';
import NProgress from 'nprogress'; //nprogress module
import 'nprogress/nprogress.css'; //styles of nprogress

//Binding events. 
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

function MyApp({ Component, pageProps }) {
    return <Component {...pageProps} />
}
export default MyApp;

代理后端 API 服务器

在 next.config.js 中配置重定向:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/proxy/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ]
  },
}

参考

  1. https://levelup.gitconnected.com/improve-ux-of-your-next-js-app-in-3-minutes-with-page-loading-indicator-3a422113304d
  2. https://github.com/vercel/next.js/discussions/14057
  3. https://nextjs.org/docs/api-reference/next.config.js/rewrites

React Hooks

使用 useState hook

useState 可以用来管理一个组件比较简单的一两个状态,如果状态多了不适合使用 useState 管理,可以使用 useReducer.

import React, {useState} from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

使用 useEffect hook

useEffect hook 用来实现一些副作用,一般可以用作页面首次加载时读取数据。

import React, {useState, useEffect} from 'react';

function App() {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(() => console.log('tick'), 1000);
    }
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}

export default App;

在 useEffect 中返回的函数会被用来做垃圾清理。另外需要注意的是,初始化的时候总会触发一次 useEffect.

默认情况下,每次 state 有改变的时候,都会调用 useEffect 函数。如果需要更改触发的时机,那么需要使用 useEffect 的第二个参数来指定监听的事件或者说状态。当第二个参数只使用一个空数组 [] 的时候就只会在组件加载的时候调用。数组中有哪些变量,表示在这些变量变化的时候调用。

使用 useContext hook

Context 用来向所有包含的元素广播状态,或者说事件,而不需要通过组件树层层传递。

  1. 首先需要通过 React.createContext 定义一个高层次 Context,
  2. 然后在最外层使用 <MyContext.Provider /> 来包裹需要接受这个 context 的所有组件。
  3. 在需要使用状态的元素中调用 const ctx = useContext(MyContext), 然后使用 ctx 访问 Context 中的值

在基于类的 React 组件中,需要使用 <MyContext.Consumer /> 来实现读取值,现在我们都用 useContext 钩子了。

const themes = {
  light: { foreground: "#000000", background: "#eeeeee" },
  dark: { foreground: "#ffffff", background: "#222222" }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div> <ThemedButton /> </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

使用 useReducer hook

useReducer 和 useState 的用途基本是一样的,但是当需要的状态比较复杂的时候,最好使用 useReducer. 有了 useReducer 钩子,基本上可以不使用 redux 了。

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 通常放在管理一组状态的根元素这个层级,比如说一个页面。dispatch 函数触发事件,reducer 函数用来处理事件,更新 state.

需要注意的是,在 useReducer 的 reducer 函数中,和 redux 不同的是,不需要 state=initialState 这个参数。默认参数在调用 useReducer 的时候已经给出了。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useReducer + useContext = (Better) Redux

可以组合使用 useReducer 和 useContext 来实现 Redux 的功能。对于某个组件自身的数据,我们只使用 useReducer 就可以很好地管理了。然而对于多个组件都需要使用的全局数据,需要是用 useContext 来广播给所有需要该数据的组件。

两者的组合使用很简单,使用 Context.Provider 把 state 和 dispatch 这两个变量广播给 root 下的所有元素,这样在需要使用 state 和 dispatch 的地方直接 useContext(Context) 就好了。

useMemo/useCallback 钩子

用来避免重复计算或者重复生成函数。

useRef 钩子

https://stackoverflow.com/questions/56455887/react-usestate-or-useref

自定义钩子

通过灵活组合 useState, 和 useEffect, 我们完全可以创建自己的钩子。

import React from 'react';

function useOffline() {
  const [isOffline, setIsOffline] = React.useState(false);

  function onOffline() {
    setIsOffline(true);
  }

  function onOnline() {
    setIsOffline(false);
  }

  React.useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);

    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);

  return isOffline;
}

function App() {
  const isOffline = useOffline();

  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }

  return <div>You are online!</div>;
}

export default App;

参考

  1. https://www.robinwieruch.de/react-hooks
  2. https://www.robinwieruch.de/react-hooks-fetch-data
  3. https://medium.com/@nazrhan.mohcine/react-hooks-work-with-usestate-and-usereducer-effectively-471646cdf925
  4. https://swizec.com/blog/usereducer-usecontext-for-easy-global-state-without-libraries

Nextjs 教程

当我们要写一个稍微复杂的 React 应用的时候,就需要路由功能了,比较流行的路由是 react router. 这是一个很好的库,但是当我们已经用到路由的时候,下一步就该考虑如何做服务端渲染了,所以直接上 next.js 吧。

鉴于我已经使用 create-react-app 创建了 react 应用,需要手工安装一下 next.js

yarn add next

把 package.json 中的 scripts 替换掉

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

核心概念

next 的核心概念是页面,没啥可解释的吧。按照约定,放在 /pages 文件夹中的每一个组件都是一个页面,比较恶心的是每个组件需要使用 export default 导出。

当然,pages/index.js 对应的自然是首页了。

function HomePage() {
  return <div>Welcome to Next.js!</div>
}

export default HomePage

然后就可以看到首页啦!啊啊啊

next.js 中,完全按照文件的物理路径来确定路由,比如如果你需要 post/1 这种路径,直接定义 pages/post/[id].js, 也是够直接了。

读取 URL 参数

需要使用 router 来手动读取

import {useRouter} from 'next/router'

function MyPage() {
  const router = useRouter();
  const {keyword} = router.query;
}

获取数据

在 nextjs 中,鼓励的方式是在服务端编译或者渲染的时候获取数据,而不是由客户端渲染数据。这里我们先不看 SSG 了,看现在最需要的 SSR.

在一个页面中,export 一个 async 函数 getServerSideProps 就可以实现获取服务端的数据。

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

context 中比较重要的几个属性:

  • params 路径中的参数,比如 {id: xxx}
  • req/res 请求响应
  • query query_string

在这个函数中,应该直接读取数据库或者外部 API.

除此之外,另一种方式自然是传统的在客户端获取数据了,可以使用 useSWR 库。

样式

next.js 中默认不让导入全局的 CSS, 所以你必须在 pages/_app.js 中导入全局的 css.

import '../styles.css'

// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

对于每一个组件,把它们的样式文件放到 [name].module.css 中就好啦。然后需要导入

import styles from 'Button.module.css';

export default function Button() {
  return <div className={styles.button}>Button</div>
}

另一种方式是使用 styled-jsx, 也就是把 CSS-in-JS 的方式,我个人还是喜欢这种方式一些。但是这种不好在 VSCode 中直接显示调色板。

<style jsx>{`
  h1 {
    color: red;
  }
`}</style>

静态文件

也很简单,直接放到 /public 目录,然后就能在根路径访问了。

路由

使用 [xxx] 放在路径中作为参数就好了。

nextjs 中的链接是这样的:

<Link href="/blog/[slug]" as={`/blog/${post.slug}`}>
    <a>{post.title}</a>
</Link>

外部接口

错误页面

next.js 可以自定义 404 和 500 错误页面

参考

  1. https://haodong.io/render-client-side-only-component-in-next-js
  2. https://github.com/vercel/next.js/blob/canary/examples/progressive-render/pages/index.js

从消息队列的角度来理解 Redux

在 React 中,组件之间是不能互相通信的,数据只能自上而下流动。所以必须把状态放在最高层的组件中。复杂一点的页面肯定会导致状态越提越高,所以最终还是需要一个单独的状态存储——redux。

Redux 的文档是非常差的。它只写了 What 和 How, 而没有写 Why. 他只说了自己是个状态存储,却不说为什么需要其他的东西,这就让初学者感到非常地迷惑,实际上它只要提到一下 “Event Bus” 这个词就非常容易理解了,非要上来就讲什么 Action/Store/Reducer/SingleSourceOfTruth 之类的。根据最高指示:代码是用来读的,不是用来装逼的,俗称 “码读不装”, 那么我们这里先来补上 redux 的 Why.

Why Redux?

Redux 本质上来说有很大一部分功能就是一个 Event Bus, 或者 Message Queue, 又或者 Pub/Sub. React 组件之间需要通信,如果让他们之间互相访问,那么就是一个 O(N^2) 的复杂度,而如果让他们通过一个中间组件,也就是 Redux 通信,那么复杂度就大大降低了,更重要的是,代码更清晰了。当然,Redux 不只是负责他们之间的通信,而且是把状态存储了下来。这两篇 文章 写得非常好了。

What is Redux?

Redux 里的概念特别多,不过知道了 EventBus, 就都非常好理解了,这里先列出来:

  • store, 存放状态的唯一容器,类似于消息队列的 broker.
  • reducer, 就是用来处理消息的函数,其实一般叫做 handler, 起个 reducer 的名字好像就函数式了,瞬间高大上加逼格高了起来。这个函数签名是 function(state, action) -> new_state.
  • action 和 dispatch, Action 其实就是消息总线里面的消息或者说事件,分发 Action, 其实不就是生产者么?
  • actionCreator, actionCreator 就是用来创建 Action 的。Action 不过就是一个 {type, value} 的字典罢了。function xxxCreator() -> action 就叫做一个 actionCreator
  • Middleware, 其实就是字面意思,会处理你的状态的一些中间件。
import {createStore} from "redux";

function reducer(state = {}, action) {
  switch (action.type) {
    case "XXX":
      return {
        ...state,
        XXX: action.value
      };
    case "YYY":
      return {

      }
    default:        
      return state;
  }
}

let store = createStore(reducer);

store.dispatch({type: "XXX", value: "XXX"})

console.log(store.getState())

以上就是一个非常基本的 redux 应用啦,如果要有多个 reducer 分开处理不同事件怎么办呢?使用 combineReducers. 需要注意的是,这样的话,每个 reducer 只会获得其中一部分数据了,比如说 userReducer 只会获得 user 部分,itemsReducer 只会处理 items 部分。

import {createStore, combineReducers} from "redux";

let reducers = combineReducers({
  user: userReducer, items: itemsReducer
})

let store = createStore(reducers)

React-Redux

在上面的例子中,实际上完全没有涉及到 react 的相关内容,redux 本身就是可以独立存在的一个库。但是,99.99% 的情况下,你还是为了管理 react 的状态采用 redux 的。要在 react 中使用 redux , 需要使用 react-redux 库。

这里需要注意的是,都 2020 年了,我们自然要用 hooks 了,所以网上好多还在讲 connect 的教程可以不用看了。几个常用的钩子:

  • useSelector, 顾名思义,用来从 store 中选取需要的状态,相当于消费者 consume
  • useDispatch, 用来 dispatch action, 相当于生产者 produce
  • useStore, 虽然有这个 hook, 但是文档里明确说了不推荐用
// yarn add react-redux
import {useSelector, useDispatch} from "react-redux";

// 这里需要注意的是,state 是整个状态树。
const result = useSelector((state) => selectedState)
// const result = useSelector((state) => state.counter)

const dispatch = useDispatch()
// 这里需要用一个匿名函数
<button onClick={() => dispatch({ type: 'increment-counter' })}>
  Increment counter
</button>

不使用 hooks 的方式

在没有 hooks 之前,需要使用 react-redux 库提供的 connect/mapState/mapDispatch 几个函数来实现 redux 和 react 组件之间的交互。

  • useSelector 对应以前的 mapState 函数
  • useDispatch 对应以前的 mapDispatch 函数

设计 redux state tree

我们知道 redux 的 state 是作为一个树存在的,设计这个树的形态是 redux 使用的重中之重了。

第一个问题,是否需要把所有的状态放进 redux 呢?不一定,对于一些组件内部的 UI 状态,比如是否隐藏某个按钮,是可以使用 useState 放在内部的。重点在于:你这个状态其他组件关心吗?一般来说,表单的状态时不需要放进 redux 的。

第二个问题,对于多个页面,比如说:一个列表页,一个详情页,一个用户页,要把所有的状态放在一个 store 中么?我倾向于把同一个组的页面的数据放在同一个 State 里面,这里的组大概就相当于 Django 的 App 或者是 Flask 的 Blueprint 的概念。当然,对于小型项目来说,用一个 store 就够了。

总的来说,分以下几步:

  1. 确定全局状态,比如用户登录状态,放在 redux 最顶级。
  2. 设计每个页面分区可视化树。根据页面布局,确定组件,以及每个组件的状态。
  3. 设计 reducers, 同样是一棵树状的组织。
  4. 实现 actions, 其中也包括加载数据的 action.
  5. 实现展示层,也就是组件们。

页面的布局图和状态树:


使用 normalized data

当我们使用 useSelector 的时候,需要从 state 出发,一层层地获取数据,所以数据的层级最好不要太多,避免出现 state.posts[0].comments[0] 这种冗长的表达式。另一方面,设计不好的状态树会导致好多状态重复存储,或者不好查询。为了解决这个问题,我们可以使用 Redux 官方推荐的解决方案。

不好的方案:

const blogPosts = [
  {
    id: 'post1',
    author: { username: 'user1', name: 'User 1' },
    body: '......',
    comments: [
      {
        id: 'comment1',
        author: { username: 'user2', name: 'User 2' },
        comment: '.....'
      },
      {
        id: 'comment2',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  },
  {
    id: 'post2',
    author: { username: 'user2', name: 'User 2' },
    body: '......',
    comments: [
      {
        id: 'comment3',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      },
      {
        id: 'comment4',
        author: { username: 'user1', name: 'User 1' },
        comment: '.....'
      },
      {
        id: 'comment5',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  }
  // and repeat many times
]

更好的组织方式

{
    posts : {
        byId : {
            "post1" : {
                id : "post1",
                author : "user1",
                body : "......",
                comments : ["comment1", "comment2"]
            },
            "post2" : {
                id : "post2",
                author : "user2",
                body : "......",
                comments : ["comment3", "comment4", "comment5"]
            }
        },
        allIds : ["post1", "post2"]
    },
    comments : {
        byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment : ".....",
            },
            "comment3" : {
                id : "comment3",
                author : "user3",
                comment : ".....",
            },
            "comment4" : {
                id : "comment4",
                author : "user1",
                comment : ".....",
            },
            "comment5" : {
                id : "comment5",
                author : "user3",
                comment : ".....",
            },
        },
        allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
    },
    users : {
        byId : {
            "user1" : {
                username : "user1",
                name : "User 1",
            },
            "user2" : {
                username : "user2",
                name : "User 2",
            },
            "user3" : {
                username : "user3",
                name : "User 3",
            }
        },
        allIds : ["user1", "user2", "user3"]
    }
}

而这些数据要放在 entities 中:

{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}

何时发送数据请求?

通过 side effects 和 thunk 与服务器通信

从上面的脚本我们可以看出,redux 完全是一个本地的消息处理,然而当我们在本地做出更改的时候,肯定需要在放到服务器啊,这种操作在 redux 中被称作 side effects(副作用), 可以使用 redux-thunk 来实现。

// 添加 thunk 支持,一般在 store.js 中
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

如果是一个组件的内部数据,那么没必要在 redux 中保存状态,也就是使用 useState 就好了。获取数据直接在 useEffect 中 fetch 数据,然后 setState 就可以了。

在 useEffect 中获取数据之后触发 action, 还是通过触发一个 action 来获取数据?这个和私有状态是一样的考虑,如果还有其他的组件会触发这个操作更新数据,那么就使用 action 最好,如果这个数据是组件私有的,那么在 useEffect 中直接获取就好。比如说,对于页面组件,可能直接在组件中使用 useEffect(fn, []) 加载数据就好了,可以直接调用 fetch, 也可以触发一个 action.

对于需要获取数据的操作,一般需要三个 Action, 分别是 FETCH_XXX_BEGIN/SUCCESS/FAILURE. 如果获取数据成功,那么 SUCCESS 的 action 中就会包含数据。另外,还要有一个 RESET_XXX_DATA 的 action, 用来清除和重置。

一个典型的加载数据的 action:

export function fetchSearchData(args) {  
    return async (dispatch) => {    
    // Initiate loading state    
    dispatch({      
      type: FETCH_SEARCH_BEGIN    
    });
    try {      
      // Call the API      
      const result = await fetch(
        "api/search/?q=xxx",
        args.pageCount, 
        args.itemsPerPage
      );           
      // Update payload in reducer on success     
      dispatch({        
        type: FETCH_SEARCH_SUCCESS,        
        payload: result,        
        currentPage: args.pageCount      
      });    
    } catch (err) {     
      // Update error in reducer on failure           
      dispatch({        
        type: FETCH_SEARCH_FAILURE,        
        error: err      
      });    
    }  
  };
}

对应的 Reducer 可以这样:

const initialState = {  payload: [],  isLoading: false,  error: {}};

export function searchReducer( state=initialState, action ) {    
  switch(action.type) {    
    case FETCH_SEARCH_BEGIN:      
      return {        
        ...state,        
        isLoading: true    
      };        
    case FETCH_SEARCH_SUCCESS:      
      return {        
        ...state,        
        payload: action.payload,        
        isLoading: false      
      };        
    case FETCH_SEARCH_FAILURE:      
      return {        
        ...state,        
        error: action.error,        
        isLoading: false            
      };
    case RESET_SEARCH_DATA:      
      return {
        ...state,
        ...initialState
      };        
    default:
      return state;
  }
}

在 class-based React 中,一般是在 ComponentWill/DidMount 周期中调用加载数据的逻辑,我们现在自然是使用 useEffect 的这个钩子来实现。

redux 项目目录结构

只要分着放 actions 和 reducers 两个目录就可以了,其实没多大要求。我一般是这样放的:

App.js
state/
  actions/
  reducers/
  actionTypes.js
  rootReducer.js
  store.js

在 index.js 中还是需要

import {Provider} from 'react-redux';
import store from 'state/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

和 React-router 一起使用

当我们和 react-router 一起使用的时候,会遇到一个问题,组件是从 URL 中读取状态,还是从 store 中读取状态呢?这两个状态之间怎么同步呢?redux 的作者 Dan 给出了答案:

不要在 redux 中保存 URL 中的状态,直接从 URL 中读取。可以把 URL 想象成另外一个单独的小的 Store.

遗留问题

  • 页面跳转/组件卸载时,是否要删除上一个页面的数据

参考

  1. Redux 的本质,Event bus
  2. https://stackoverflow.com/questions/50703862/react-redux-state-shape-for-multiple-pages
  3. https://stackoverflow.com/questions/37288070/designing-redux-state-tree
  4. https://stackoverflow.com/questions/33619775/redux-multiple-stores-why-not
  5. Redux 中文基础教程
  6. https://www.pluralsight.com/guides/how-to-structure-redux-components-and-containers
  7. https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase
  8. 使用 hooks 编写 redux
  9. https://medium.com/@gaurav5430/async-await-with-redux-thunk-fff59d7be093
  10. https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
  11. The best way to architect your Redux app
  12. Where and When to Fetch Data With Redux
  13. https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/
  14. How to sync Redux state and url query params
  15. https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

如何加载数据

  1. https://stackoverflow.com/questions/51113369/how-to-dispatch-fetched-data-from-api
  2. https://stackoverflow.com/questions/39419237/what-is-mapdispatchtoprops
  3. You have to include dispatch in useEffect because of lint rules
  4. https://stackoverflow.com/questions/38206477/react-router-redux-fetch-resources-if-needed-on-child-routes
  5. https://www.freecodecamp.org/news/loading-data-in-react-redux-thunk-redux-saga-suspense-hooks-666b21da1569/
  6. https://stackoverflow.com/questions/57097390/react-hooks-fetch-data-inside-useeffect-warning
  7. https://stackoverflow.com/questions/57925027/useeffect-goes-in-infinite-loop-when-combined-usedispatch-useselector-of-redux
  8. https://stackoverflow.com/questions/62167174/need-clarification-for-react-react-redux-hooks-middleware-thunk-fetching-ap

其他的状态管理

  1. https://github.com/jamiebuilds/unstated-next
  2. https://imweb.io/topic/5a453691a192c3b460fce36e
  3. https://news.ycombinator.com/item?id=12371248
  4. https://github.com/mobxjs/mobx/issues/199
  5. https://blog.rocketinsights.com/redux-vs-mobx/

学习 React

当我第一次接触前端的时候,那时候流行的是后端 MVC 模式。过去写界面的方法是,把所有的结构 (html),动作 (js),样式 (css) 分开,好处是非侵入,离了谁都能工作,缺点是无法模块化。在 js 无足轻重,甚至有 noscript 这种插件的过去,显然 MVC 是最佳实践,但是到了 js 大行其道的今天,把 html/js/css 打包在一起的模块化又被提出来了。

react 中也没有模板中的 {% block xxx %} 这个概念,直接使用 props。

Hello World

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

Jsx 中可以使用大括号插值。对于 html 中不能自闭合的标签,都可以自闭合。JSX 最终会被编译成 JavaScript.

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// 相当于
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

一个函数就可以是一个组件。在 React 16 时代,就不要再用 class 了,统一用函数式组件就好了。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 相当于
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

props 只有向下传递一种方式。所有的函数都必须是纯函数。使用函数作为组件的一个缺点是没有办法保存状态。

在 class 组件中,需要使用使用 setState 更新状态。不能使用 += 类似的操作符,setState 是异步的,因此要传递回调函数。

事件

react 的事件和 HTML 的不同。所有属性是 camelCase 的;不能通过 return false 来阻止事件,必须调用 e.preventDefault

<button onClick={activateLasers}>
  Activate Lasers
</button>

一般情况下,在 React 中是不需要调用 addEventListener

条件渲染

React 是 functional 的。所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。state 是组件内部的状态。

key 是一个很关键的概念,有点像是 html 中的 ID, 用来唯一标示一个元素,因为 react 会尽可能复用元素。但是 key 不需要是全局唯一的,只需要在兄弟元素之间唯一即可。

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];

ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

组合

在 React 中不要使用继承来组织组件,而要使用组合,这也是近几年来面向对象领域的趋势。在 React 中,可以通过读取 props.children 来获取传递进来的子组件。

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

如果需要对子组件布局的话,可以使用命名的方式:

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane left={ <Contacts /> } right={ <Chat /> } />
  );
}

另一种方式是特化,也就是类似函数的 partial

Thinking in React

React 设计哲学:https://zh-hans.reactjs.org/docs/thinking-in-react.html


  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

已经过期的一些知识

由于 JS 的 this 的坑,需要使用 public class fields。如果要向回调函数中使用参数需要这样:onClick={(e) => this.deleteRow(id, e)}

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  handleClick = () => {
    console.log('this is:', this);
  }
render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

如果需要传递参数的话:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>

参考资料

  1. https://medium.com/@Zwenza/functional-vs-class-components-in-react-231e3fbd7108
  2. https://segmentfault.com/a/1190000011474522
  3. https://stackoverflow.com/questions/22876978/loop-inside-react-jsx