Skip to content

现在框架的特性: 数据驱动更新生命周期组件化路由监听数据JSX组件传值(子传父、父传子、全局值交互)配合编译器webpack、vite、rollup等等。

没说错吧,不管是vuereact,又或者微信小程序都是以上几种特性组合而成的。本文会结合vue做对比的方式提出两者的差异点,如果你刚好会vue,正在学习react那么本篇文章应该会对你受益很大。如果你是资深使用react的,可以指出我总结下错误的地方。

路由篇

路由来说,咱们需要知道以下这些就够用了:

  1. 怎么定义路由
  2. 指定路由的界面
  3. 如何切换路由
  4. 如何获取路由的参数,以及获取参数

定义路由

如果你是使用官方推荐的React Router (v7)生成的脚手架npx create-react-router@latest,那么使用上就可以像vue的配置式一样了如下:

ts
// routes.ts
import {
  type RouteConfig,
  index,
  route,
  layout,
  prefix,
} from "@react-router/dev/routes";

export default [
  layout("./components/Layout.tsx", [
    index("routes/home.tsx"),
    route("user", "./views/user.tsx"),
    route("user/:name", "./views/user/detail.tsx"),
  ]),

  ...prefix("concerts", [index("./views/concerts/user.tsx")]),
] satisfies RouteConfig;
  • index就是代表根路径,直接是个文件地址就行
  • route代表子路由,第一个参数是路由地址,第二个参数是路由组件地址
  • layout代表布局,第一个参数是布局的组件地址,第二个参数是各个路由
tsx
// Layout.tsx
import { Outlet } from "react-router";

const AppLayout = () => {
  return (
    <div className="flex">
      <div className="w-1/5">
        <App />
      </div>
      <div className="w-4/5">
        <Outlet />
      </div>
    </div>
  );
};

使用Outlet代表路由放的位置。有点类似于Vue<router-view />的样子

  • prefix很明显了就是前缀的意思,类似于vue的baseUrl

以上定义的直接通过运行的地址访问到了

bash
http://localhost:5173/
http://localhost:5173/user
http://localhost:5173/user/3367
http://localhost:5173/concerts

使用从零构建一个react项目的npm create vite@latest my-app -- --template react可以通过安装react-router的方式去使用如下:

安装react-router

yarn add react-router

在你从零构建一个react项目中main.jsx中如下:

jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";

import App from "./App.jsx";
import About from "./views/About.jsx";
import AppLayout from "./components/layout/index.jsx";
import Page1 from "./views/Page1.jsx";
import Page2 from "./views/Page2.jsx";
import ConcertsIndex from "./views/concerts/Index.jsx";
import City from "./views/concerts/city.jsx";
import Trending from "./views/concerts/trending.jsx";

import { BrowserRouter, Routes, Route } from "react-router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="about" element={<About />} />

        {/* layout */}
        <Route element={<AppLayout />}>
          <Route path="page1" element={<Page1 />} />
          <Route path="page2" element={<Page2 />} />
        </Route>

        {/* prefix */}
        <Route path="concerts">
          <Route index element={<ConcertsIndex />} />
          <Route path=":city" element={<City />} />
          <Route path="trending" element={<Trending />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </StrictMode>,
);

使用<BrowserRouter /> 、<Routes />、<Route /> 来配合使用就行了基本跟上面一样,定义路由名称path,指定组件element

可以使用react-router中的<BrowserRouter /> 、<HashRouter />来切换路由的hash、history的模式

BrowserRouterhistory 模式, 即: http://localhost:5173/home

jsx
// BrowserRouter 是 history 模式
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { BrowserRouter, Routes, Route } from "react-router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="home" element={<App />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
);

HashRouterhash 模式, 即: http://localhost:5173/#/home

jsx
// BrowserRouter 是 hash 模式
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { HashRouter, Routes, Route } from "react-router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <HashRouter>
      <Routes>
        <Route path="home" element={<App />} />
      </Routes>
    </HashRouter>
  </StrictMode>,
);

这里在提下路由懒加载吧,react中提供了lazySuspense组件来支持路由懒加载,使用起来非常方便。如下包裹:

jsx
import { StrictMode, lazy, Suspense } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";

import App from "./App.jsx";
import About from "./views/About.jsx";
import AppLayout from "./components/layout/index.jsx";
import Page1 from "./views/Page1.jsx";
import Page2 from "./views/Page2.jsx";
import ConcertsIndex from "./views/concerts/Index.jsx";
import City from "./views/concerts/city.jsx";
import Trending from "./views/concerts/trending.jsx";

const LazyAbout = lazy(() => import("./views/About.jsx"));

import { BrowserRouter, Routes, Route } from "react-router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<App />} />
          <Route path="about" element={<About />} />

          {/* layout 布局*/}
          <Route element={<AppLayout />}>
            <Route path="page1" element={<Page1 />} />
            <Route path="page2" element={<Page2 />} />
          </Route>

          {/* prefix 路由前缀 */}
          <Route path="concerts">
            <Route index element={<ConcertsIndex />} />
            <Route path=":city" element={<City />} />
            <Route path="trending" element={<Trending />} />
          </Route>

          {/* lazy 路由懒加载*/}
          <Route path="lazy-about" element={<LazyAbout />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  </StrictMode>,
);

切换路由

react-router中,切换路由可以使用useNavigate

jsx
// nav/index.jsx

import React from "react";
import { useNavigate } from "react-router";
import { Button, Flex } from "antd";

const NavIndex = () => {
  const navigate = useNavigate();

  const onClick1 = () => {
    navigate("/nav/page1");
  };

  const onClick2 = () => {
    navigate("/nav/page2");
  };

  const onClick3 = () => {
    navigate("/nav/123");
  };

  return (
    <>
      <Flex gap="small" wrap>
        <Button type="primary" onClick={onClick1}>
          去到 /nav/page1页面
        </Button>
        <Button type="primary" onClick={onClick2}>
          去到 /nav/page2页面
        </Button>
        <Button type="primary" onClick={onClick3}>
          去到 /nav/:id 页面
        </Button>
      </Flex>
    </>
  );
};

export default NavIndex;

main.jsx中增加

jsx
import { StrictMode, lazy, Suspense } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";

import App from "./App.jsx";
import About from "./views/About.jsx";
import AppLayout from "./components/layout/index.jsx";
import Page1 from "./views/Page1.jsx";
import Page2 from "./views/Page2.jsx";
import ConcertsIndex from "./views/concerts/Index.jsx";
import City from "./views/concerts/city.jsx";
import Trending from "./views/concerts/trending.jsx";

import NavIndex from "./views/nav/Index.jsx";
import NavPage1 from "./views/nav/Page1.jsx";
import NavPage2 from "./views/nav/Page2.jsx";
import NavPage3 from "./views/nav/Page3.jsx";

const LazyAbout = lazy(() => import("./views/About.jsx"));

import { BrowserRouter, Routes, Route } from "react-router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<App />} />
          <Route path="about" element={<About />} />

          {/* layout */}
          <Route element={<AppLayout />}>
            <Route path="page1" element={<Page1 />} />
            <Route path="page2" element={<Page2 />} />
          </Route>

          {/* prefix */}
          <Route path="concerts">
            <Route index element={<ConcertsIndex />} />
            <Route path=":city" element={<City />} />
            <Route path="trending" element={<Trending />} />
          </Route>

          <Route path="lazy-about" element={<LazyAbout />} />

          {/* 路由切换相关代码 */}
          <Route path="nav">
            <Route index element={<NavIndex />} />
            <Route path="page1" element={<NavPage1 />} />
            <Route path="page2" element={<NavPage2 />} />
            <Route path=":id" element={<NavPage3 />} />
          </Route>
        </Routes>
      </Suspense>
    </BrowserRouter>
  </StrictMode>,
);

navigate('/nav/page1');做为路由跳转,navigate(-1);可以做为路由返回。

路由参数传参获取

路由传参数有以下几种方式:

  • 直接拼接到路由上 navigate('/nav/page1?id=1&name=zhangsan');
  • 使用 navigate state参数传递更复杂的数据(例如对象或数组)。这些数据不会出现在 URL 中,但可以在目标页面中访问,有点像是全局存储了一份数据。
  • 路由params传参数 使用useParams接收

第一种:navigate('/nav/page1?id=1&name=zhangsan');方式使用react-router中的useSearchParams获取 如下:

jsx
// nav/page1.jsx
import React from "react";
import { useNavigate, useSearchParams } from "react-router";
import { Button } from "antd";
const Page1 = () => {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  const onBack = () => {
    navigate(-1);
  };
  console.log(searchParams.get("id"), searchParams.get("name"), "id,name"); // 输出: 1 zhangsan id,name

  return (
    <>
      <div>
        <h1>Nav Page1</h1>

        <Button type="primary" onClick={onBack}>
          返回
        </Button>
      </div>
    </>
  );
};

export default Page1;

第二种:navigate state参数传递更复杂的数据如下,这种方式是利用history、hashAPI做数据存储的都是存在你浏览器记录里面了,所以不能把链接分享给别人去使用

jsx
const onClick2 = () => {
  navigate("/nav/page2", {
    state: {
      id: 2,
      name: "lisi",
    },
  });
};

获取方式如下:

jsx
// nav/page2.jsx
import React from "react";
import { useNavigate, useLocation } from "react-router";
import { Button } from "antd";
const Page2 = () => {
  const navigate = useNavigate();

  const location = useLocation();
  const state = location.state || {};

  console.log(state, "state"); // 输出:{ "id": 2, "name": "lisi" }  'state'

  const onBack = () => {
    navigate(-1);
  };
  return (
    <>
      <div>
        <h1>Nav Page2</h1>
        <Button type="primary" onClick={onBack}>
          返回
        </Button>
      </div>
    </>
  );
};

export default Page2;

第三种:路由params传参数

jsx
const onClick3 = () => {
  navigate("/nav/123");
};
jsx
import React from "react";
import { useNavigate, useParams } from "react-router";
import { Button } from "antd";
const Page3 = () => {
  const navigate = useNavigate();
  const { id } = useParams();
  console.log(id, "id"); // 输出:123 id

  const onBack = () => {
    navigate(-1);
  };
  return (
    <>
      <div>
        <h1>Nav Page3</h1>
        <Button type="primary" onClick={onBack}>
          返回
        </Button>
      </div>
    </>
  );
};

export default Page3;

路由篇总结

其实看到这里也算是完全掌握路由相关的使用了,咱们接着往下看

组件编写以及JSX使用

组件的模块化,一处编写,多处使用。咱们接下来先编写一个简单的react 组件

jsx
// start/Index.jsx
import React from "react";

export default function Start() {
  return (
    <div>
      <h1>About1</h1>
      <h1>About2</h1>
    </div>
  );
}

Fragment组件使用

不想要父元素(去掉一层元素)div包裹可以使用Fragment组件,如下:

jsx
import React, { Fragment } from "react";

export default function Start() {
  return (
    <Fragment>
      <h1>About</h1>
      <h1>About2</h1>
    </Fragment>
  );
}

Fragment可以简写如下:

jsx
import React from "react";

export default function Start() {
  return (
    <>
      <h1>About</h1>
      <h1>About2</h1>
    </>
  );
}

JSX 基础使用方式

jsx
import React from "react";

export default function Start() {
  const data = [
    {
      name: 1,
      id: 1,
    },
    {
      name: 2,
      id: 2,
    },
  ];

  const currentType = true;

  const onBtnClick = () => {
    console.log("我是按钮点击了click");
  };

  return (
    <>
      <h1>About</h1>
      <h1>About2</h1>
      {/* 循环渲染数据 */}
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <ul>
        {data
          .filter((item) => item.name !== 1)
          .map((item) => (
            <li key={item.id}>{item.name}</li>
          ))}
      </ul>

      {/* 三目运算 */}
      <div>{currentType ? <h1>true 111111</h1> : <h1>false22222</h1>}</div>

      {/* 添加class类名 */}

      <div></div>

      {/* 行内样式 */}
      <div style={{ color: "red", fontSize: "16px" }}>我是什么颜色</div>

      {/* 事件绑定 click*/}
      <button onClick={onBtnClick}>我是按钮</button>
    </>
  );
}

父子组件传参数

其实在react中省去了子传父 Vue 中 emit事件了,直接利用props传一个函数方法作为回调去做的,如下:

jsx
// parent.jsx

import React, { useState } from 'react';

import Children from './children';

const Parent = () => {
  const [name, setName] = useState('张三');

  return (
    <div>
      <h1>About Page</h1>
      <Children name={name} onClick={setName}/>
    </div>
  );
};


export default Parent;



// children.jsx
import React from 'react';

const Children = ({ name, onClick}) => {
  return (
    <div>
      <h1>{ name }</h1>
      <button onClick={() => {onClick('李四')}}> 点击触发【子传父】</button>
    </div>
  );
};


export default Children;

以上就是一个简单的组件传值交互,就是把name给子组件去使用,并且把函数setName传了下去,做为点击事件的回调函数。

第二种方案是向下透传,类似于vuePrvider、inject,使用useContext、createContext

第一步创建index.js

js
import { createContext } from "react";
// 创建上下文
export const DataContext = createContext(null);

第二步使用这个上下文方法

jsx
// index.jsx
import React, { useState } from "react";
import { DataContext } from './index'

import Children from "./children";

 const ContextPage = () => {
 const [name, setName] = useState('张三');


  return (
    // 注意这里是使用的 DataContext.Provider 并且必须是value字段,value 可以是字符串,对象,数组等
    <DataContext.Provider value={{ name, setName }}>
       <Children />
    </DataContext.Provider>
  );
};


export default ContextPage;

//children.jsx
import React, { useContext } from 'react';
import { DataContext } from './index'

const Children = () => {
    // useContext 方法入参数就是前面定义好的上文DataContext,context接收后可以直接使用就行
  const context = useContext(DataContext);

  return (
    <div>
      <h1>{ context.name }</h1>
      <button onClick={() => {context.setName('李四')}}> 点击触发【子传父】</button>
    </div>
  );
};


export default Children;

useContext、DataContext是透传数据,不止父子组件,只要是父级<DataContext.Provider>包裹内以下任何一级使用,上面咱们传输了一个对象name, setName然后就可以给子级组件去使用了,注意绑定在<DataContext.Provider>上的一定是value

生命周期

在使用react hooks中省去生命周期,可以直接用useState、useEffect去代替。这里先不去着重讲这几个hooks的用法,请继续往下看

  • useState 代表初始化状态 类似于VueonCreated,其实在VUE组合式API中生命周期也基本可以省去了。
  • useEffect 代表Vue中的onMounted、onDestroyed事件

简单实用如下:

jsx
export const Demo = () => {
  console.log("组件初始化");
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("组件挂载了");
    // 比如使用了window.addEventListener
    return () => {
      console.log("组件卸载了");
      // 比如使用了window.addEventListener 这里可以使用window.removeEventListener清楚掉
    };
  });
};

插槽

react对组件位置的存放更简单,不用定义固定的位置(<slot />)或者说具名插槽(<slot name="footer" />),可以直接使用如下:

先创建layout.jsx文件:

jsx
import React from "react";

export default function Layout({ children, headerComponent }) {
  return (
    <div>
      <header>{headerComponent}</header>
      <main>{children}</main>
    </div>
  );
}

再创建一个header.jsx文件以及main.jsx文件如下:

jsx
// header.jsx`

import React from "react";


const Header = () => {
    return (
        <div>我是头部Header部分代码</div>
    )
}

export default Header

//main.jsx

import React from "react";

const Main = () => {
    return (
        <div>我是主体Main部分</div>
    )
}

export default Main

接下来直接创建index.jsx文件展示用法

jsx
import React from "react";

import Main from "./main";
import Header from "./header";
import Layout from "./layout";

const SoltPage = () => {
  return (
    <Layout headerComponent={<Header />}>
      <Main></Main>
    </Layout>
  );
};

export default SoltPage;

Layout组件中的参数children部分就是插槽的默认部分,向Layout直接传了一个<Header/>,然后在Layout组件就直接用了{headerComponent},也挺省事的。

react 样式编写

react中样式使用css、less、scss跟在vue中一样,直接下载依赖,然后直接创建对应的.less、.scss、.css文件以import引入到对应的组件即可,比如import './index.css'; import './index.less'; import './index.scss'

但是以上没有scope组件样式隔离的效果,假如类名一样等情况会相互影响的。

react经常使用的样式隔离方案就是使用styled-components插件,这里咱们简单过下如何使用,如果对原理有兴趣可以参考我之前《为什么在vue中style-components没有火起来?》文章

下载插件:yarn add styled-components, 如果是vscode编辑器可以下载插件vscode-styled-components,然后就能像写css样有代码提示了 使用如下:

jsx
import React from "react";
import styled from "styled-components";

// 写完就是一个组件,可以直接使用
const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #f0f0f0;
  min-height: calc(100vh - 40px);
`;

const StylePage = () => {
  return (
    <Container>
      <div>
        <h1>StylePage</h1>
      </div>
    </Container>
  );
};

export default StylePage;

还可以直接更改子元素的样式如下:

jsx
import React from "react";
import styled from "styled-components";

const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #f0f0f0;
  min-height: calc(100vh - 40px);

  h1 {
    color: red;
  }
`;

const StylePage = () => {
  return (
    <Container>
      <div>
        <h1>StylePage</h1>
      </div>
    </Container>
  );
};

export default StylePage;

还可以进行props传参数使用如下:

jsx
import React from "react";
import styled from "styled-components";

const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #f0f0f0;
  min-height: calc(100vh - 40px);

  h1 {
    color: red;
  }
`;

const Button = styled.button`
  background-color: #007bff;
  color: ${(props) => props.color};
  border: none;
  padding: 10px 20px;
  border-radius: 5px;
  cursor: pointer;
  font-size: 16px;
`;
const StylePage = () => {
  return (
    <Container>
      <div>
        <h1>StylePage</h1>
        <Button color="white">Click Me</Button>
        <Button color="red">Click Me</Button>
        <Button color="blue">Click Me</Button>
      </div>
    </Container>
  );
};
export default StylePage;

还可以跟组件一样嵌套使用如下:

jsx
import React from "react";
import styled from "styled-components";

const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #f0f0f0;
  min-height: calc(100vh - 40px);

  h1 {
    color: red;
  }
`;

const Button = styled.button`
  background-color: #007bff;
  color: ${(props) => props.color};
  border: none;
  padding: 10px 20px;
  border-radius: 5px;
  cursor: pointer;
  font-size: 16px;
`;

const HoverButton = styled(Button)`
  transition: background-color 0.3s ease;

  &:hover {
    background-color: #0056b3;
  }
`;
const StylePage = () => {
  return (
    <Container>
      <div>
        <h1>StylePage</h1>
        <Button color="white">Click Me</Button>
        <Button color="red">Click Me</Button>
        <Button color="blue">Click Me</Button>

        <HoverButton color="white">Click Me</HoverButton>
        <HoverButton color="red">Click Me</HoverButton>
        <HoverButton color="blue">Click Me</HoverButton>
      </div>
    </Container>
  );
};

export default StylePage;

以上就是styled-components插件的使用,还有更改用法更多请看官方文档, 还有使用tailwindcss的方式,大致就是定义好了类名,直接去使用就行(个人不是很喜欢这种)。

Hooks 核心应用

以下内置的几个hooks重重之重每个都要必须熟练掌握 useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRef,我接下来会针对每个的使用场景以及注意事项用法都仔细的讲一遍

useState

useState是用于在函数组件中管理组件的状态,它返回一个状态值和一个更新状态的函数。使用如下:

以下就是一个简单的例子,点击按钮+1

jsx
import React from "react";
import { Button, Divider } from "antd";

const UseStatePage = () => {
  const [count, setCount] = React.useState(0);
  return (
    <>
      <div>
        <Button onClick={() => setCount(count + 1)}>点击+1</Button>
        <h1>{count}</h1>
      </div>
      <Divider />
    </>
  );
};

export default UseStatePage;

再来看一个案例点击按钮如果触发两次setCount2(count2 + 1)会怎么样?

jsx
import React from "react";
import { Button, Divider } from "antd";

const UseStatePage = () => {
  const [count, setCount] = React.useState(0);
  const [count2, setCount2] = React.useState(0);
  return (
    <>
      <div>
        <Button onClick={() => setCount(count + 1)}>点击+1</Button>
        <h1>{count}</h1>
      </div>
      <Divider />

      <div>
        <h1>陷阱:错误❌的使用示例</h1>
        <Button
          onClick={() => {
            setCount2(count2 + 1);
            setCount2(count2 + 1);
          }}
        >
          按钮2: 点击+1+1
        </Button>
        <h1>{count2}</h1>
      </div>
    </>
  );
};

export default UseStatePage;

以上每次点击按钮2: 点击+1+1还是每次+1,不会按照预期+2,因为react的执行时机是异步的。请再接着往下看

jsx
// 加一个定时器
const [count3, setCount3] = React.useState(0);
<div>
  <h1>陷阱:错误❌的使用示例</h1>
  <Button
    onClick={() => {
      setCount3(count3 + 1);

      setTimeout(() => {
        setCount3(count3 + 1);
      }, 500);
    }}
  >
    点击+1+1
  </Button>
  <h1>{count3}</h1>
</div>;

加上定时器也是一样的不会每次加2,官方的解释是:这是因为 状态表现为就像一个快照。更新状态会使用新的状态值请求另一个渲染,但并不影响在你已经运行的事件处理函数中的 count JavaScript 变量。那么如何解决这个问题呢?

传入一个函数即可:(请注意a更改是函数的入参数,不是原来的count4)

jsx
const [count4, setCount4] = React.useState(0);
<div>
  <h1>正确✅使用示例</h1>
  <Button
    onClick={() => {
      setCount4((a) => a + 1);
      setCount4((a) => a + 1);
    }}
  >
    点击+1+1
  </Button>
  <h1>{count4}</h1>
</div>;

Object类型要如何更改数据呢?请看以下示例

jsx
import React, { useState } from "react";
import { Button } from "antd";

const UseStateOther = () => {
  const [count, setCount] = useState({
    name: "zhangsan",
    age: 18,
  });

  return (
    <>
      <h1> Object 类型使用示例</h1>
      <Button
        onClick={() => {
          setCount({
            ...count,
            age: count.age + 1,
          });
        }}
      >
        age + 1
      </Button>
      <pre>{JSON.stringify(count, null, 2)}</pre>
    </>
  );
};

export default UseStateOther;

setCount({...count, age: count.age + 1 }),就是把原来的参数使用...,然后再去更改age的值,那么有没有更方便的更改方式呢?能直接更改就生效了呢?

如下咱们简单写个useImmer方法用来便捷的更改Object类型,使之能直接使用count.age++来更新

jsx
const useImmer = (initState) => {
  const [state, setState] = useState(initState);

  const setStateImmer = (callback) => {
    const data = typeof state === "object" ? { ...state } : state;
    if (typeof callback === "function") {
      callback(data);
    }
    setState(data);
  };

  return [state, setStateImmer];
};

大致就是对useStatesetData包裹了一层方法用来处理Object等类型的更新,使用如下:

jsx
 const [data2, setData2] = useImmer({
    name: 'zhangsan',
    age: 18
  });

   <div>
     <h1> Object 简单使用示例</h1>
     <Button onClick={() => {
        setData2((draft) => {
          draft.age++
        })
      }}>
        age + 1
      </Button>
      <pre>{JSON.stringify(data2,null,2)}</pre>
    </div>
    <Divider />

上面只是简单的useImmer的函数封装,应该还有好多没有考虑到的情况,大家可以直接使用官方推荐的immerjs/use-immer 依赖,直接安装去使用就行了。

接下来咱们对数组的数据处理方式也做个简单介绍,对数组的增加、删除、改,我下面就直接使用use-immer依赖宝使用了,如下:

jsx
import React from "react";
import { Button, Divider } from "antd";
import { useImmer } from "use-immer";

const UseStateArray = () => {
  const [data, setData] = useImmer([
    {
      name: "zhangsan",
      age: 18,
    },
  ]);

  return (
    <>
      <div>
        <h1>数组 增加</h1>
        <Button
          onClick={() => {
            setData((draft) => {
              draft.push({
                name: "lisi",
                age: 18,
              });
            });
          }}
        >
          加一条数据
        </Button>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>

      <Divider />

      <div>
        <h1>数组删除最后一条数据</h1>
        <Button
          onClick={() => {
            setData((draft) => {
              draft.splice(-1, 1);
            });
          }}
        >
          减一条数据
        </Button>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>

      <Divider />

      <div>
        <h1>数组对第一条数据age + 1</h1>
        <Button
          onClick={() => {
            setData((draft) => {
              draft[0].age++;
            });
          }}
        >
          age++
        </Button>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>
    </>
  );
};

export default UseStateArray;

useState 总结: 如果更改基本类型可以直接使用useState,如果需要更改数组、对象等更便捷可以使用useImmer方法。

useEffect

注意再开发模式中由于react的严格机制,useEffect会被更新2次,主要是怕你使用不当导致出现的bug

  • useEffect 第二个字段为空数组时,可以当作onMounted去使用,只在首次触发,不做任何监听。
  • useEffect 第二个字段不为空数组时,可以当作onUpdated去使用,监听到该数组内数据变化,会触发。
  • useEffect 第二个字段不传时,只要该组件被reload就会被触发(该组件任意值更新)。
  • useEffect 第一个参数return一个函数,可以当作onUnmounted去使用。

useEffect可以把它当作onMounted、onUnmounted、onUpdated、去使用。使用如下:

jsx
// indx.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from 'antd'
import Children from "./children";

const UseEffectPage = () => {
    const [count, setCount] = useState(0);
    const [data, setData] = useState(0);

    useEffect(() => {
      setData((a) => a + 1)
    }, [])

    return (
        <>
          <h1>基本使用</h1>
          <Button onClick={() => setCount(count + 1)}>点击更改值</Button>
          <h3>{count}</h3>

          <h2>useEffect的触发次数: {data}</h2>
          <Children count={count} />
          <Divider />
        </>
    )
};

export default UseEffectPage;

// children.jsx
import React, { useEffect } from "react";

const Children = ({count}) => {
  const [data, setData] = React.useState(0);

  useEffect(() => {
    setData((a) => a + 1)
  }, []);

  return (
    <div>
      <h1>{count}</h1>
      <h2>useEffect子组件更新的次数{data}</h2>
    </div>
  );
};

export default Children;

在点击按钮时,由于useEffect为空,不做任何监听,不会产生触发。

所以在useEffect第二个参数为空时,就可以单纯当作onMounted去使用。

useEffect如果监听count,在点击按钮时就可以看到更新了(每次加1),如下:

jsx
useEffect(() => {
  setData((a) => a + 1);
}, [count]);

如果第二个参数直接不填会怎么样?如下:

jsx
// children.jsx
import React, { useEffect } from "react";

const Children = ({ count }) => {
  const [data, setData] = React.useState(0);

  useEffect(() => {
    // setData((a) => a + 1)
    console.log("子组件更新了");
  });

  return (
    <div>
      <h1>{count}</h1>
      <h2>useEffect子组件更新的次数{data}</h2>
    </div>
  );
};

export default Children;

其实会看到每次父组件更改count值后,子组件进行reload,由于useEffect第二个参数没传,导致跟子组件一样,每次触发2次。

  • useEffect 第一个参数return一个函数,可以当作onUnmounted去使用,示例如下
jsx
// children.jsx
import React, { useEffect } from "react";

const Children = ({ count }) => {
  const [data, setData] = React.useState(0);

  useEffect(() => {
    setData((a) => a + 1);

    return () => {
      console.log("useEffect子组件卸载");
    };
  }, []);

  return (
    <div>
      <h1>{count}</h1>
      <h2>useEffect子组件更新的次数{data}</h2>
    </div>
  );
};

export default Children;

请再看下面一个示例:

jsx
// index.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from 'antd'
import Children from "./children";

const UseEffectPage = () => {
    const [count, setCount] = useState(0);
    const [data, setData] = useState(0);
    console.log('父组件更新了');

    useEffect(() => {
      setData((a) => a + 1)
    }, [count])

    return (
        <>
          <h1>基本使用</h1>
          <Button onClick={() => setCount(count + 1)}>点击更改值</Button>
          <h3>{count}</h3>

          <h2>useEffect的触发次数: {data}</h2>
          <Divider />

          <Children />
          <Divider />
        </>
    )
};

export default UseEffectPage;


// children.jsx
import React, { useEffect } from "react";
import { Button, Divider } from 'antd'

const Children = () => {
  const [data, setData] = React.useState(0);
  const [count1, setCount] = React.useState(0);

  console.log('子组件更新了');

  useEffect(() => {
    setData((a) => a + 1)

    return () => {
      console.log("useEffect子组件卸载");
    };
  }, []);

  return (
    <div>
      <h2>useEffect子组件更新的次数{data}</h2>
      <Divider />

      <Button onClick={() => setCount(count1 + 1)}>子组件+1</Button>
      <h3>子组件{count1}</h3>
    </div>
  );
};

export default Children;

可以把关注点看在两个console.log('父组件更新了');、console.log('子组件更新了');,在点击了父子组件的按钮,会怎样执行,在这里讲下react的更新机制

  • 父组件中有字段更新更新,就会触发父组件以及子组件(没用props字段也会)reload
  • 子组件字段有更新,当前子组件会被重reload,父组件不会从新reload

那么有没有办法解决父组件更新,子组件不用reload呢?也是有的哈可以使用mome函数包裹子组件一层。如下:

jsx
import React, { useEffect, memo } from "react";
import { Button, Divider } from "antd";

const Children = memo(() => {
  const [data, setData] = React.useState(0);
  const [count1, setCount] = React.useState(0);

  console.log("子组件更新了");

  useEffect(() => {
    setData((a) => a + 1);

    return () => {
      console.log("useEffect子组件卸载");
    };
  }, []);

  return (
    <div>
      <h2>useEffect子组件更新的次数{data}</h2>
      <Divider />

      <Button onClick={() => setCount(count1 + 1)}>子组件+1</Button>
      <h3>子组件{count1}</h3>
    </div>
  );
});

export default Children;

这个时候父组件有更新,子组件就不会更新了。除非是子组件上有参数更新了,子组件才会更新,比如:

jsx
// index.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from "antd";
import Children from "./children";

const UseEffectPage = () => {
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(0);
  const [data, setData] = useState(0);

  console.log("父组件更新了");

  useEffect(() => {
    setData((a) => a + 1);
  }, []);

  return (
    <>
      <h1>基本使用</h1>
      <Button onClick={() => setCount(count + 1)}>点击更改值count</Button>
      <h3>{count}</h3>

      <Button onClick={() => setCount1(count1 + 1)}>点击更改值count1</Button>
      <h3>{count1}</h3>

      <h2>useEffect的触发次数: {data}</h2>
      <Divider />

      <Children count={count} />
      <Divider />
    </>
  );
};

export default UseEffectPage;

// children.jsx
//这里跟上面一样,请注意 我把count绑定在子组件上

在点击点击更改值coun后发现父子组件都做reload了。 在点击点击更改值count1后就父组件reload,子组件不会。

**总结:**关于reload的组件更新机制刚才也说了,如何利用mome优化也有使用示例,useEffect的第二个参数不传、为空数组、为有数据相关都有说明示例。

useRef

注意这里跟vueref超级不一样react 中改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息。如有需要,使用useState代替。

写个错误示例: 这里的count值会一直为0,因为useRefcurrent值不会触发reload

jsx
// index.jsx
import React, { useRef, useEffect } from "react";
import { Button } from "antd";

const useRefPage = () => {
  const count = useRef(0);
  console.log("组件重新渲染!!!");

  useEffect(() => {
    console.log(count.current);
  }, [count]);

  return (
    <div>
      <h1>错误❌示例</h1>
      <Button
        onClick={() => {
          count.current++;
        }}
      >
        Ref点击加加
      </Button>
      <h1>{count.current}</h1>
    </div>
  );
};

export default useRefPage;

useRef更适合用来绑定dom,存储定时器, 存储信息数据,比如:

jsx
import { useState, useRef } from "react";

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>开始</button>
      <button onClick={handleStop}>停止</button>
    </>
  );
}

以上示例用useRef来存储定时器,useRefcurrent值不会触发reload。并且在组件重新渲染(更新state)时,useRefcurrent值不会改变。

在或者用来操作DOM,如下:

jsx
import React, { useRef, useEffect } from "react";

function MyComponent() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 页面加载完成后聚焦到输入框
    inputRef.current.focus();
    console.log(inputRef.current.value); // 获取输入框的值
  }, []);

  return <input ref={inputRef} />;
}

警告⚠️:useRef不能像Vueref绑定在组件上 可以获取子组件的值。可以传输给子组件,交给子组件去绑定dom,如下:

jsx
//index.jsx
const componentRef = useRef(null);
<DomRef ref={componentRef} />;

// Dom.jsx

import React, { useRef, useEffect } from "react";

const DomRef = ({ ref }) => {
  const inputRef = useRef(null);

  useEffect(() => {
    // 页面加载完成后聚焦到输入框
    inputRef.current.focus();
    console.log(inputRef.current.value); // 获取输入框的值
  }, []);

  return (
    <>
      <h1>绑定DOM 示例</h1>
      <input ref={inputRef} />
      <a ref={ref}>我是A标签</a>
    </>
  );
};
export default DomRef;

请注意⚠️: 如果你是react v18以及以下版本,需要用forwardRef函数包裹下才能向下传输ref值,如下:(v19该API已废弃可以像上面直接传输ref)

jsx
import { forwardRef } from "react";

const DomRef = forwardRef(({ ref }) => {
  const inputRef = useRef(null);

  useEffect(() => {
    // 页面加载完成后聚焦到输入框
    inputRef.current.focus();
    console.log(inputRef.current.value); // 获取输入框的值
  }, []);

  return (
    <>
      <h1>绑定DOM 示例</h1>
      <input ref={inputRef} />
      <a ref={ref}>我是A标签</a>
    </>
  );
});
export default DomRef;

useReducer

参数描述 const [state, dispatch] = useReducer(reducer, initialArg, init?)

  • state 是当前状态值
  • dispatch 是一个函数,用于触发状态更新,它接收一个参数 action,可以是一个对象或一个函数。
  • reducer 是一个函数,它接收两个参数 stateaction,并根据 action 的类型返回一个新的状态值。
  • initialArg 是一个可选参数,用于初始化状态值,如果提供了 init 函数,则 initialArg 将作为 init 函数的参数。

简单使用示例如下:

jsx
// index.jsx
import { useReducer } from "react";

function reducer(state, action) {
  if (action.type === "incremented_age") {
    return {
      ...state,
      age: state.age + 1,
    };
  }
  throw Error("Unknown action.");
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42, name: "Joe" });

  return (
    <>
      <button
        onClick={() => {
          dispatch({ type: "incremented_age" });
        }}
      >
        Increment age
      </button>
      <p>
        Hello {start.name}! You are {state.age}.
      </p>
    </>
  );
}

还可以把dispatch传给子组件去触发<Children dispatch={dispatch}/>

猛的一看不就useStatesetSate放在了reducer函数上了,其实也确实这个样,这样会让数据状态管理更加清晰,集中,方便维护。

咱们以useState、useReducer 分别实现一个对数据{ age: 42, name: 'Joe' }的更改,可以做个对比如下:

jsx
// reducer.jsx
import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      ...state,
      age: state.age + 1
    };
  }

  if(action.type === 'decremented_age') {
    return {
      ...state,
     age: state.age -1
    };
  }

  if(action.type === 'change_name') {
    return {
      ...state,
     name: '张三'
    };
  }
  throw Error('Unknown action.');
}

export default function ReducerCounter() {
  const [state, dispatch] = useReducer(reducer, { age: 42, name: 'John' });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        增加年龄
      </button>
      <button onClick={() => {
        dispatch({ type: 'decremented_age' })
      }}>
        减少年龄
      </button>
      <button onClick={() => {
        dispatch({ type: 'change_name' })
      }}>
        更改名字
      </button>
      <p>Hello{state.name}! You are {state.age}.</p>
    </>
  );
}

// state.jsx
import { useState } from 'react';


export default function StateCounter() {
  const [data, setData] = useState({ age: 42, name: 'John' });

  const incremented = () => {
    setData({
        ...data,
        age: data.age + 1
    })
  }
  const decremented = () => {
    setData({
        ...data,
        age: data.age - 1
    })
  }
  const change = () => {
    setData({
        ...data,
        name: '张三'
    })
  }

  return (
    <>
      <button onClick={incremented}>
        增加年龄
      </button>
      <button onClick={decremented}>
        减少年龄
      </button>
      <button onClick={change}>
        更改名字
      </button>
      <p>Hello{data.name}! You are {data.age}.</p>
    </>
  );
}

可以看出useReducer其实把数据处理更加聚合,集中,清晰了。

use-immer依赖包中也有useImmerReducer可以直接使用,咱们把上面的useReducer改成useImmerReducer可以看到会更清晰一些,如下:

jsx
import React from "react";
import { useImmerReducer } from "use-immer";
function reducer(draft, action) {
  switch (action.type) {
    case "incremented_age":
      draft.age++;
      break;
    case "decremented_age":
      draft.age--;
      break;
    case "change_name":
      draft.name = "张三";
      break;
    default:
      throw Error("Unknown action.");
  }
}

export default function ImmerReducerCounter() {
  const [state, dispatch] = useImmerReducer(reducer, { age: 42, name: "John" });

  return (
    <>
      <h1>useImmerReducer 管理</h1>
      <button
        onClick={() => {
          dispatch({ type: "incremented_age" });
        }}
      >
        增加年龄
      </button>
      <button
        onClick={() => {
          dispatch({ type: "decremented_age" });
        }}
      >
        减少年龄
      </button>
      <button
        onClick={() => {
          dispatch({ type: "change_name" });
        }}
      >
        更改名字
      </button>
      <p>
        Hello{state.name}! You are {state.age}.
      </p>
    </>
  );
}

注意事项:

    1. reducer方法中第一个值在没有使用useImmerReducer是不可以更改的
    1. 请确定好type类型,避免使用**魔法字符串(incremented_age、decremented_age、change_name)**去判断,可以使用一个枚举类型,或者一个MAP,减少类型错误的发生

useContext

这个其实在父传子哪里有说有具体用法,这里就不多说了可以直接在上面查看<DataContext.Provider value=>

useContext可以搭配useReducer做向下深度交互,咱们以前面的useReducer示例用 useContext来重新写下:

jsx
// index.js
import { createContext } from 'react'
export const DataContext = createContext()

// index.jsx
import React from "react";
import { DataContext } from "./index";
import { useImmerReducer } from 'use-immer'

import ViewComponent from './view'
import OperatorButton from "./operator";
function reducer(draft, action) {
    switch (action.type) {
        case 'incremented_age':
            draft.age++
            break;
        case 'decremented_age':
            draft.age--
        break;
        case 'change_name':
            draft.name = '张三'
        break;
        default:
          throw Error('Unknown action.');
    }
}

const UseContextPage = () => {
   const [state, dispatch] = useImmerReducer(reducer, { age: 42, name: 'John' });

  return (
    <DataContext.Provider
    value={{
      state,
      dispatch
    }}>
      <OperatorButton />
      <ViewComponent />
    </DataContext.Provider>
  );
};

export default UseContextPage;

// view.jsx
import React, { useContext } from "react";
import { DataContext } from './index'

const ViewComponent = () => {
    const { state } = useContext(DataContext);
  return (
    <div>
     <p>Hello{state.name}! You are {state.age}.</p>
    </div>
  );
};

export default ViewComponent;

// operator.jsx
import React, { useContext } from "react";
import { DataContext } from './index'

const OperatorButton = () => {
    const { dispatch } = useContext(DataContext);

  return (
    <>
    <h1>useContext + useReducer  管理</h1>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        增加年龄
      </button>
      <button onClick={() => {
        dispatch({ type: 'decremented_age' })
      }}>
        减少年龄
      </button>
      <button onClick={() => {
        dispatch({ type: 'change_name' })
      }}>
        更改名字
      </button>
    </>
  )
};

export default OperatorButton;

以上只是一个简单使用useContext的示例,其实如果只是子组件需要使用上下文的数据直接通过props传值就行,如果是在需要父组件透传数据的场景下用useContext比较好,像主题切换、国际化等等场景下。

useMome

useMomeVuecomputed很是类似,不同点就是第二个参数需要写下const cachedValue = useMemo(calculateValue, dependencies)

  • calculateValue 要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。
  • dependencies 所有在 calculateValue 函数中使用的响应式变量组成的数组。
jsx
import React, { useState, useMemo } from "react";

const UseMomePage = () => {
  const [count, setCount] = useState(1);
  const [count1, setCount1] = useState(2);

  const doubleCount = useMemo(() => {
    return count * count1;
  }, [count, count1]);

  return (
    <div>
      <h1>useMemo 使用示例</h1>
      <button onClick={() => setCount(count + 1)}>点击更改count+1</button>
      <button onClick={() => setCount1(count1 + 1)}>点击更改count1+1</button>

      <p>当前count值:{count}</p>
      <p>当前count1值:{count1}</p>
      <p>当前useMemo后doubleCount值:{doubleCount}</p>
    </div>
  );
};

export default UseMomePage;

像前面的示例中如果第二个参数不仅仅传递[count],确实只能监听到count更新才能从新计算了,但是代码中会有警告(React Hook usemo缺少一个依赖项:‘count1’。要么包含它,要么删除依赖项),因为count1没有被监听到。

注意为了保证calculateValue是一个纯函数,react在开发模式下会默认触发两次该函数,为你避免使用错误,及时发现bug

useCallback

useCallbackuseMemo很像,不同点是useCallback返回的是一个函数,useMemo返回的是一个值。

在下面一种场景中使用useCallback可以记忆一下setCount避免重复更新子组件

jsx
import React, { useState, useCallback } from "react";

// 子组件
const ChildComponent = React.memo(({ onIncrement }) => {
  console.log("子组件重新渲染!!");

  return <button onClick={onIncrement}>点击+1</button>;
});

// 父组件
function ParentComponent() {
  const [count, setCount] = useState(0);

  // 使用 useCallback 包裹回调函数
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 依赖数组为空,表示回调函数只在组件挂载时创建一次

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onIncrement={increment} />
    </div>
  );
}

export default ParentComponent;

useCallbackuseMemomome函数都是React提供的用于优化性能的,合理的运用会有性能显著提升
`

react proxy 代理

vue中是直接更改vue.config.js中的devServer.proxy去配置代理的,其实内部也是使用的http-proxy-middleware这个插件实现的,感兴趣可以去看看如何用代理http、https服务的。

react中需要手动下载这个插件yarn add http-proxy-middleware,然后在src目录下创建setupProxy.js用法跟vue的一样。

js
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    "/api",
    createProxyMiddleware({
      target: "http://localhost:5000",
      changeOrigin: true,
      pathRewrite: {
        "^/api": "", // 去掉请求路径中的 `/api` 前缀
      },
    }),
  );

  app.use(
    "/api2",
    createProxyMiddleware({
      target: "http://localhost:5001",
      changeOrigin: true,
      pathRewrite: {
        "^/api2": "",
      },
    }),
  );
};

React使用TS

因为react支持ts确实比较好,大多数公司都会选择react + ts开发,我觉得如果你都学到这里了ts也可以简单学学,都是一些基础的东西。

在我刚写ts的时候,领导告诉我说ts是一种思想,你可以把所有的代码都有提示,鼠标放上去就知道怎么穿参数,以及对象引用都可以有代码提示,写代码不要太爽。

刚学初期只要保证不是用any,保证每个代码都是有类型提示的就行,类型体操可以等你写熟练了后,其实就跟你写编程一样,把类型定义写的更加抽象了(这里建议适当,不要太抽象)

下一期我会单独出一篇ts的相关应用,就不在这里多说了

总结

咱们从路由->组件JSX编写->组件传值->插槽->样式编写->常用Hooks的相关细节应用->代理等各个方面做了详细的讲解。以上全部吸收后,写react完全没有问腿, 希望会对你有所帮助。

相关截图: 示意图