React 使用过程中常见问题 II

由于之前的《React 使用过程中常见问题》已经比较旧,现结合最近小伙伴们遇到的问题整理出第二版:),如下:

目录

React 中的 useEffect 怎么用?
React 中的 useMemo 怎么用?
现代 React 函数组件的 ref 如何直接操控?
React Hook 到底是什么鬼?
React 设置多个 className
React 如何覆盖子组件的样式
React 中 useState 中的 setState 设值是异步的如何在设完值后获取最新值?
React 中 useState 中的 setState 的另一种用法
React 中如何使用类 componentDidMount 及如何取到 useEffect 中被监听变量的旧值?
React 中如何使用 React.createContext 进行组件之间由外到内单向传值?
React 版 Ant Design 3.x 的那些坑 1.Popconfirm
React 版 Ant Design 3.x 的那些坑 2.Form.Item 里的 getFieldDecorator
熟悉 React 顶层 API,如 cloneElement isValidElement React.Children
react 兼容 IE9 +
React 页面数据守卫(路由、浏览器关闭、浏览器崩溃)
Create React App 项目 eject 后常见配置问题
credentials,withCredentials 接口请求配置
Webpack 中编写自定义 loader 和 plugin, 及如何调试 (debug) webpack, create-react-app 等脚手架
React 中的 useSelector 怎么用?useSelector 取出的值永远是旧值,不更新怎么处理?
React 中的 useState,组件深层次使用时,值为数组类型,更新内部某孙值时,Dom UI 不更新怎么处理?

React 中的 useEffect 怎么用?

主要是用来实现监听状态变量的变化而执行相应的回调事件,代码如下:

 useEffect(() => {
    // 处理自定义事情
  }, [variable1, variable2, variableX]); // 被 watch 的对象

注:如果不传任何变量或者,传空数组,则等效于 ReactcomponentDidMount 或者 VueonMount

React 中的 useMemo 怎么用?

主要是用来实现监听状态变量的变化而实时处理自定义计算返回最新的计算值,代码如下:

  const myComputedData = useMemo(() => {
    let result;
    // 处理自定义计算
    // 如: result = variable1 + variable2 + variableX;
    return result;
  }, [variable1, variable2, variableX]);

总之:与 VueComputed 的效果差不多。

现代 React 函数组件的 ref 如何直接操控?

React 的函数组件 ref 操作起来相对麻烦点。当然如果是 React 的类组件使用 ref 其实与 Vue 差不多。但现在还在用 React 组件的写法已经不香了。所以麻烦点也是值得的,主要依赖于:wrappedComponentRef, useRef, useImperativeHandle, forwardRef,代码如下:

// 子组件
const ChildComp = forwardRef((props, ref) => {
 // ... 其它代码
 useImperativeHandle(ref, () => ({
    // 供父组件使用的函数
    outFn1: () => {
      // 这里编写自定义代码
      return xxx;
    },
    // 供父组件使用的变量
    outData1,
    outData2,
    outDataX,
  }));
  return (
    <>
<div>content1</div>
<div>content2</div>
    </>
  );
});

// 父组件
const ParentComp = (props) => {
  const childRef = useRef({});
  // mouted 或者按需时使用子组件传过来的变量或者函数
  useEffect(() => {
    const {
      outData1,
      outData2,
      outDataX,
      outFn1,
    } = childRef.current;
    // 这里编写自定义代码 ...
    
  }, []);
  return (
<div className={style.parent}>
      <ChildComp wrappedComponentRef={childRef} />
    </div>
  );
};

除此之外,还有一种情况可以用 ref,参考官网:https://reactjs.org/docs/refs-and-the-dom.html
You can, however, use the ref attribute inside a function component as long as you refer to a DOM element or a class component:

function CustomTextInput(props) {
  // textInput must be declared here so the ref can refer to it
  const textInput = useRef(null);
  
  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

React Hook 到底是什么鬼?

对标 Vue 2.x 其实是算是一种代码组织方式或者是一种设计模式如 Vue 的 Mixin 或者 Vue 跟进的 Hook。由最底层来看,Hook 其实顾名思义是一个勾子,一个函数,创建了一个 React 组件组件闭包,返回一些变量或者函数供 React 组件内部使用,由于使用了一系列 Reactive 的变量数据的变动及流量都是响应式的,非常灵活方便。

React 设置多个 className

方法一:ES6 模板字符串 “
className={`title ${index === this.state.active ? 'active' : ''}`}
方法二:join(“”)
className={["title", index === this.state.active?"active":null].join(' ')}
方法三:classnames (需要下载 classnames)

const classNames = require('classnames');
 
const Button = React.createClass({
  // ...
  render () {
    const btnClass = classNames({
      btn: true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

参考引用:https://blog.csdn.net/qq_35605231/article/details/84974029

React 如何覆盖子组件的样式

使用 :global ,但要注意不要污染全局,要带上当前页面的 wrap 样式

.wrap {
	:global {
	 /* 按需加需要覆盖的样式 */
	}
}
/* 或者 */
:global(.myTargetClassName) {
 /* 按需加需要覆盖的样式 */
}

React 中 useState 中的 setState 设值是异步的如何在设完值后获取最新值?

使用 ref ref.current = targetState

const [data, setData] = useState({});
const refData = useRef({});

refData.current = data;
setData({a: 1}); // 设值
console.log(data); // => {},取不到
setTimeout(()=>{
  console.log(data); // => {},还是取不到
  console.log(refData.current); // => {a: 1},取到最新值了 :)
}, 1000);

React 中 useState 中的 setState 的另一种用法

useState hook 自定义 setState 函数(数组第二个参数)的使用场景,实现 data 的自定义拼装

const [data, setData] = useState({});
setData((oldData)=>{
  // ...
  return {...oldData, newKey: newValue}; // 实现 data 的自定义拼装
})

React 中如何使用类 componentDidMount 及如何取到 useEffect 中被监听变量的旧值?

定义 mounted

const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

定义一个记录旧值的 hook usePrevious

// 参考引用:https://stackoverflow.com/questions/53446020/how-to-compare-oldvalues-and-newvalues-on-react-hooks-useeffect
import { useRef, useEffect } from 'react';

const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

export default usePrevious;

使用 usePrevious 用来记录旧值

// ...
import { usePrevious } from '$myKooks';
// ...
const [val, setVal] = useState(initialVal);
const prevVal = usePrevious(val);
useEffect(() => {
	if (mounted && prevVal !== val) {
	  // 处理自定义事件
	}
}, [val]);

React 中如何使用 React.createContext 进行组件之间由外到内单向传值?

使用 React.createContext 主要是解决以前爷->父->子->孙组件如果通过 props 传值比较恶心的问题,可以一步到位(当然:现在还可以使用 useContext 这个 hook 一步到位取到值,可参考:https://reactjs.org/docs/hooks-reference.html#usecontext)。两种代码分别如下:
父组件:

import React from 'react';
import Son from './son'; // 引入子组件
// 创建一个 theme Context,
export const {Provider,Consumer} = React.createContext("默认名称");
const Parent = (props) => {
	let name ="小人头"
	return (
		// Provider 共享容器接收一个 name 属性,并可以向下 Son 子组件传递
		<Provider value={name}>
			<div style={{border:'1px solid red',width:'30%',margin:'50px auto',textAlign:'center'}}>
				<p>父组件定义的值:{name}</p>
				<Son />
			</div>
		</Provider>
	);
}
export default Parent;

子组件:

import React from 'react';
import { Consumer } from './Parent'; // 引入父组件的 Consumer 容器
import Grandson from './grandson; // 引入子组件
const Son = (props) => {
    return (
        // Consumer 容器,可以拿到上文传递下来的 name 属性, 并可以展示对应的值,并且往孙也传递了下去
        <Consumer>
            {( name ) =>
                <div style={{ border: '1px solid blue', width: '60%', margin: '20px auto', textAlign: 'center' }}>
                    <p>子组件。获取父组件的值:{name}</p>
                    {/* 孙组件内容 */}
                    <Grandson />
               </div>
            }
        </Consumer>
    );
}
export default Son;

孙组件:

import React from 'react';
import { Consumer } from './Parent'; // 引入父组件的 Consumer 容器
const Grandson = (props) => {
    return (
         // Consumer 容器,可以拿到上文传递下来的 name 属性,并可以展示对应的值
        <Consumer>
            {(name ) =>
                   <div style={{border:'1px solid green',width:'60%',margin:'50px auto',textAlign:'center'}}>
                   <p>孙组件。获取传递下来的值:{name}</p>
               </div>
            }
        </Consumer>
    );
}
export default Grandson;

使用 useContext 方式:

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

const ThemeContext = React.createContext(themes.light);
// 暴露到外部一步到位的 useContext 方式
export const useThemeContext = () => {
  return React.useContext(ThemeContext) || {};
};

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

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

function ThemedButton() {
  // const theme = React.useContext(ThemeContext);
  const theme = useThemeContext(); // 一步到位的 useContext 方式
  
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

React 版 Ant Design 3.x 的那些坑 1.Popconfirm

Popconfirm 里不能隔一层 <> 会导致点击失效

<Popconfirm
   title="确认删除吗?"
   onConfirm={(e) => {
   // 删除
   }}
>
  <>
    <div>
      点我试试
	</div>
  </>
</Popconfirm>

React 版 Ant Design 3.x 的那些坑 2.Form.Item 里的 getFieldDecorator

getFieldDecorator 里不能隔一层 div 或者其他,会导致取值失败,错误如下

<Form.Item label="Title">
  {getFieldDecorator('title', {
	rules: [{ required: true, message: 'Please input the title of collection!' }],
  })(
  <div>
	<Input />
  </div>
  )}
</Form.Item>

熟悉 React 顶层 API,如 cloneElement isValidElement React.Children 等待

参考引用:https://zh-hans.reactjs.org/docs/react-api.html

// 验证对象是否为 React 元素,返回值为 true 或 false。
React.isValidElement(object)

// 以 element 元素为样板克隆并返回新的 React 元素。config 中应包含新的 props,key 或 ref。
// 返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有
// 的子元素,如果在 config 中未出现 key 或 ref,那么原始元素的 key 和 ref 将被保留。
React.cloneElement(
  element,
  [config],
  [...children]
)

// 将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。
// 当你想要在渲染函数中操作子节点的集合时,它会非常实用,特别是当你想要在向下
// 传递 this.props.children 之前对内容重新排序或获取子集时。
React.Children.toArray(children)

react 兼容 IE9 +

参考引用:https://www.jianshu.com/p/98318e411acc
1.安装 react-app-polyfill 和 core-js
npm install react-app-polyfill core-js
2.index.js 引用:

 
import 'core-js/es'  
import 'react-app-polyfill/ie9'  
import 'react-app-polyfill/stable'

3.修改package.json

   
   "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all",
            "ie > 9"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version",
            "ie > 9"
        ]
    },
    "devDependencies": {}

一般做到这里 就可以了, 但是有些电脑IE文档模式 默认值是 7,
通过 F12—>仿真—>文档模式 进行查看; 这个时候需要在打包好的 index.html 或者 在 public 文件夹下的 index.html head里 加上一行:

<meta http-equiv="X-UA-Compatible" content="IE=edge">

React 页面数据守卫(路由、浏览器关闭、浏览器崩溃)

1、数据未保存,如何防止路由意外后退?使用 react 自带组件 Prompt
import { Prompt } from 'react-router-dom';

 
      <Prompt
        when={yourBooleanCondition}
        message={() => '退出页面未保存,会导致正在编辑的数据丢失'}
      />

2、数据未保存,如何防止浏览器关闭?使用 beforeunload 监听
window.addEventListener('beforeunload', listenerCallback);
3、数据未保存,浏览器崩溃如何恢复数据?使用 componentDidCatch 或者 window.onerror
即:componentDidCatch(error, info)window.removeEventListener('error', listenerCallback);
要注意策略:因为都不流行 class component 所以可以用 window.onerror 比较多。
1)先使用备份机制备份好数据(定时或者按需),量大可以备份到 sessionStorage
2)崩溃时(react 崩溃时可通过延时判断 #root 下元素有没有 mount 成功,即 Dom 内不为空),数据转存到 localStorage
3)下次打开时,凭 localStorage 判断是否提示恢复数据。

Create React App 项目 eject 后常见配置问题

1、使用 webpackbar 显示打包或者构建进度条,舒服
1)安装, yarn add webpackbar --dev
2)配置 webpack.config.js

 
      // ...
      const WebpackBar = require('webpackbar');
	  // ...
      plugins: [
          new WebpackBar(), // 使用
      // Generates an `index.html` file with the <script> injected.
	  // ...
	  ]

2、使用 thread-loader 配置多线程打包
1)安装, yarn add thread-loader --dev
2)配置 webpack.config.js

            // ...
            // Process application JS with Babel.
            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
            {
              test: /\.(js|jsx|ts|tsx)$/,
              exclude: /node_modules/, // 排除了,可以优化打包速度
              include: paths.appSrc,
              use: [
                "thread-loader",
                {
                  loader: require.resolve("babel-loader"),
				  // ...
				}
			  ]
			 },
			{
              test: /\.mjs$/,
              include: /node_modules/,
              type: "javascript/auto",
            },

3、安装 @babel/plugin-proposal-decorators decorators 支持 @ 修饰符的使用
1)修改 package.json 文件,增加

{
  // ...
  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ]
    ]
  }
  // ...
}

2)修改或者增加 .eslintrc.js 文件

module.exports = {
  root: true,
  extends: ['react-app'],
  rules: {
    'linebreak-style': [0, 'error', 'windows'],
    'react/react-in-jsx-scope': 'off',
    'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.js'] }], // 解决 index.js 中不能使用 JSX
    'max-len': ['warn', 10000],
    'no-unused-vars': 'off',
    'no-useless-escape': 'off',
    'react-hooks/exhaustive-deps': 'off',
    'import/no-anonymous-default-export': 'off',
    'no-mixed-operators': 'off',
    'array-callback-return': 'off',
    'jsx-a11y/anchor-is-valid': 'off',
    'no-restricted-globals': 'off',
    'no-control-regex': 'off',
  },
  parserOptions: {
    parser: 'babel-eslint',
    "ecmaFeatures": {
      "legacyDecorators": true // 主要是这个选项
    }
  }
};

3)配置 .env 文件(本地开发第个成员可再添加 .env.local 文件, 需 gitignore)与 env.js,把必要环境参数暴露给浏览器使用, 可与 node 环境共享使用
.env 文件配置

# 接口可能使用的前缀
PRODUCTION_API_URL_PREFIX=http://www.production_xxx.com

env.js 文件配置

// ...
function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter((key) => REACT_APP.test(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        // Useful for determining whether we’re running in production mode.
        // Most importantly, it switches React into the correct mode.
        NODE_ENV: process.env.NODE_ENV || "development",
        // Useful for resolving the correct path to static assets in `public`.
        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
        // This should only be used as an escape hatch. Normally you would put
        // images into the `src` and `import` them in code to get their paths.
        PUBLIC_URL: publicUrl,
        // We support configuring the sockjs pathname during development.
        // These settings let a developer run multiple simultaneous projects.
        // They are used as the connection `hostname`, `pathname` and `port`
        // in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
        // and `sockPort` options in webpack-dev-server.
        WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
        WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
        WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
        // Whether or not react-refresh is enabled.
        // react-refresh is not 100% stable at this time,
        // which is why it's disabled by default.
        // It is defined here so it is available in the webpackHotDevClient.
        FAST_REFRESH: process.env.FAST_REFRESH !== "false",
        PRODUCTION_API_URL_PREFIX: process.env.PRODUCTION_API_URL_PREFIX, // 添加这里,浏览器可能通过 process.env.PRODUCTION_API_URL_PREFIX 取到值
      }
    );
// ...

使用范例

// ...
const service = axios.create({
  baseURL: process.env.NODE_ENV === 'development' ? '/' : process.env.PRODUCTION_API_URL_PREFIX, // api 的 base_url
  timeout: 30000,
  method: "post",
  credentials: 'include',
  withCredentials: true,
  headers: {
    "Content-type": "application/json; charset=utf-8",
  },
});
// ...

4)配置 husky 与 lint-staged 结合 eslint, stylelint, prettier 规范提交代码
pre-commit 文件

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged 
# 按需,可能不是 yarn

.lintstagedrc.js 文件

// .lintstagedrc.js 文件中的代码如下
module.exports = {
  "src/**/\*{js,jsx,ts,tsx,html}": ["eslint", "prettier --write"],
  "src/**/\*{.scss,.less,.css}": ["stylelint --fix"],
}

详细可参考:https://www.jianshu.com/p/a7cea983e7a2
5)配置 .editorconfig .eslintrc.js .prettierrc .prettierignore .stylelintrc.json .yarnrc.yml src/setupProxy.js .lintstagedrc.js .env .env.local .gitignore package.json docker 目录 .dockerignore 文件

credentials,withCredentials 接口请求配置

fetch / axios 中加上 credentials: 'include' 反而会报错 跨域问题
Access-Control-Allow-Origin 的值为 '*' 的时候跨域请求不允许携带认证
那么只能在服务端修改 Access-Control-Allow-Origin 的值,改成你指定的允许跨域访问的域的值

axios 跨域 withCredentials 设置与后段设置
axios.defaults.withCredentials = true 在请求头里带上 cookie
如果前端配置了这个 withCredentials = true,后段设置 Access-Control-Allow-Origin 不能为 '*', 必须是源地址。如:

header("Access-Control-Allow-Origin", "源地址");
header("Access-Control-Allow-Credentials", "true");

Webpack 中编写自定义 loader 和 plugin, 及如何调试 (debug) webpack, create-react-app 等脚手架

调试分为两种:
浏览器中调试
vscode中调试

如 create-react-app 中的 package.json 配置 scripts 启动调试命令:
"debug-webpack": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --config webpack.config.js"

更多可参考:https://blog.csdn.net/fesfsefgs/article/details/119983556

React 中的 useSelector 怎么用?useSelector 取出的值永远是旧值,不更新怎么处理

这个 hook 超好用,如果有用过 vuex 的 getters, 大概就这个意思,就是去统一的 state 里按需取数,如下,可以直接取 item1 来用了

  const {
    list,
	item1,
  } = useSelector(state => ({
    list: state.list.info,
    item1: state.list.find(item => item.id === 'id1'),
  }));

useSelector 取出的值永远是旧值,不更新怎么处理?使用 useLayoutEffect 结合

  // 接着上面的代码
  const listRef = useRef(list);
  
  useLayoutEffect(() => {
    listRef.current = list;
  }, [list]);
  
  // 下面使用 listRef.current 就可以取到最新的值了

React 中的 useState,组件深层次使用时,值为数组类型 或 对象,更新内部某孙值时,Dom UI 不更新怎么处理?

这画面似层相识对吧?没错,vue 2 使用数据类型为数组时也会有类似的情况,react 其实也会有类似的情况。
【方式一】处理方式其实比较简单。我们更新数据时,换一个数组 / 对象吧。代码如下:

  // 定义处
  const [arr, setArr] = useState([]);
  const [obj, setObj] = useState({});
  
  // 更新处
  arr[1].x = 1;
  setArr([...arr]); // 换一个数组吧
  
  obj.x = 1;
  setObj({...obj}); // 换一个对象吧

【方式二】上面方式还是不行?
使用延时渲染,思路类似如下:

    setTableDataSource([]); // 先清空,如 antd 的 Table 绑定了 tableDataSource
	setLoading(true); // 结合 loading 体验好点

    setTimeout(() => {
      setTableDataSource(myDataSource); // 延时再赋值
	  setLoading(false);
    }, 500);

交流与学习
本文作者:Nelson Kuang,欢迎大家留言及多多指教
版权声明:欢迎转载学习 => 请标注信息来源于 http://www.kt5.cn/fe/2021/08/26/react-ii/