2022 React 最速上手指南(十四)—— 最终篇

简介: 2022 React 最速上手指南(十四)—— 最终篇

以结果为导向,写给刚学完前端三剑客和想要了解 React 框架的小伙伴,使得他们能快速上手(省略了历史以及一些不必要的介绍)。



状态进阶


如果你不熟悉 JS 中的 Reducer,可以先看这篇博客:What is a Reducer in JavaScript/React/Redux


我们可以使用 useReducer hook 替换掉大量的 useState hook 来简化【状态管理】,先在组件外部引入一个 reducer 函数,通过接收两个参数 state 和 action 返回一个新的 state。


// (state, action) => newState
const storiesReducer = (state, action) => {
  if (action.type === "SET_STORIES") {
    return action.payload;
  } else {
    throw new Error();
  }
};
复制代码


useReducer hook 接收一个 reducer 函数和一个初始 state 作为参数,与 useState 类似,它返回一个包含两项内容的数组:


  • 第一项是当前 state
  • 第二项是用于更新 state 的函数(也叫 dispatch 函数)


// 替换掉 const [stories, setStories] = React.useState([]);
const [stories, dispatchStories] = React.useRe
ducer(storiesReducer, []);
复制代码


用新的 dispatch 函数替换掉原本的 setStories 函数:


const App = () => {
  ...
  const [stories, dispatchStories] = React.useReducer(storiesReducer, []);
  React.useEffect(() => {
    setIsLoading(true);
    getAsyncStories()
      .then((res) => {
        // 传递一个 action 对象
        dispatchStories({
          type: "SET_STORIES",
          payload: res.data.stories,
        });
        setIsLoading(false);
      })
      .catch(() => setIsError(true));
  }, []);
  const handleRemoveStory = (item) => {
    const newStories = stories.filter(
      (story) => item.objectID !== story.objectID
    );
    dispatchStories({
      type: "SET_STORIES",
      payload: newStories,
    });
  };
  ...
};
复制代码


与 useState 的更新函数不同,dispatch 函数需要传递给 reducer 一个【action 对象】,其中包含 type 和可选的 payload,以便 reducer 进行匹配。

我们还可以把功能封装在 reducer 中,然后用 action 进行多个 state 的管理:


const storiesReducer = (state, action) => {
  // 使用 switch 语句增加代码可读性
  switch (action.type) {
    case "SET_STORIES":
      return action.payload;
    case "REMOVE_STORY":
      // 返回一个新的 state
      return state.filter(
        (story) => action.payload.objectID !== story.objectID
      );
    default:
      throw new Error();
  }
};
const App = () => {
  ...
  const handleRemoveStory = (item) => {
    dispatchStories({
      type: "REMOVE_STORY",
      payload: item,
    });
  };
  ...
};
复制代码


当我们连续使用了 state 更新函数后,就有可能【导致不合理状态】,从而引发 UI 的问题。


如果我们获取数据出错:


// 模拟没有获取到数据
const getAsyncStories = () =>
  new Promise((resolve, reject) => setTimeout(reject, 2000));
getAsyncStories().catch(() => setIsError(true));
复制代码


你会看到屏幕上同时显示了错误信息和无休止的加载信息,也就是说 isError 更新了,但 isLoading 没有更新,这显然是不合理的。


image.png


为了避免这种情况,我们可以把这些状态合并到同一个 useReducer hook 中,之后所有异步数据的相关操作都通过 dispatch 函数来更新:


const storiesReducer = (state, action) => {
  switch (action.type) {
    case "STORIES_FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false,
      };
    case "STORIES_FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case "STORIES_FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    case "REMOVE_STORY":
      return {
        ...state,
        // 需要操作 state.data 而不是 state
        data: state.data.filter(
          (story) => action.payload.objectID !== story.objectID
        ),
      };
    default:
      throw new Error();
  }
};
const App = () => {
  ...
  const [stories, dispatchStories] = React.useReducer(
    storiesReducer, 
    { 
      data: [],
      isLoading: false,
      isError: false,
    }
  );
  React.useEffect(() => {
    dispatchStories({ type: "STORIES_FETCH_INIT" });
    getAsyncStories()
      .then((result) => {
        dispatchStories({
          type: "STORIES_FETCH_SUCCESS",
          payload: result.data.stories,
        });
      })
      .catch(() => dispatchStories({ type: "STORIES_FETCH_FAILURE" }));
  }, []);
  const handleRemoveStory = (item) => {
    dispatchStories({
      type: "REMOVE_STORY",
      payload: item,
    });
  };
  ...
  // stories.data
  const searchedStories = stories.data.filter((story) =>
    story.title.toLowerCase().includes(searchTerm.toLowerCase())
  );
  return (
    <>
      ...
      {stories.isError && <p>Something went wrong ...</p>}
      {stories.isLoading ? (
        <p>Loading...</p>
      ) : (
        <List list={searchedStories} onRemoveItem={handleRemoveStory} />
      )}
    </>
  );
};
复制代码


每次转换 state 都会返回一个新的 state 对象,其中包含【被新属性值覆盖的】当前 state 的全部键值对(展开运算符)。


获取数据


使用真正的第三方API Hacker News API 来获取数据,删掉原来的 initialStories 和 getAsyncStories 函数:


const API_ENDPOINT = 'https://hn.algolia.com/api/v1/search?query=';
const App = () => {
  ...
  React.useEffect(() => {
    dispatchStories({ type: "STORIES_FETCH_INIT" });
    // query=react
    fetch(`${API_ENDPOINT}react`)
      .then(response => response.json())
      .then((result) => {
        dispatchStories({
          type: "STORIES_FETCH_SUCCESS",
          payload: result.hits,  // API数据
        });
      })
      .catch(() => dispatchStories({ type: "STORIES_FETCH_FAILURE" }));
  }, []);
  ...
};
复制代码


通过 fetch API 获取 React 相关的新闻数据,将数据转换为 JSON 发送给组件 state。

我们可以将原有的搜索功能升级一下,从客户端搜索改为服务端搜索,用 searchTerm 作为动态查询条件请求 API,获取一组被服务器筛选的列表:


const App = () => {
  const [searchTerm, setSearchTerm] = useSemiPersistentState(
    "search", "React"   // 初始值为 React
  );
  ...
  React.useEffect(() => {
    // 也可以写成 if (!searchTerm)
    if (searchTerm === "") return;
    dispatchStories({ type: "STORIES_FETCH_INIT" });
    // query={searchTerm}
    fetch(`${API_ENDPOINT}${searchTerm}`)
      .then((response) => response.json())
      .then((result) => {
        dispatchStories({
          type: "STORIES_FETCH_SUCCESS",
          payload: result.hits,
        });
      })
      .catch(() => dispatchStories({ type: "STORIES_FETCH_FAILURE" }));
  }, [searchTerm]);   // 依赖数组改变
  // 删掉跟 searchedStories 有关的函数
  ...
  return (
    <>
      ...
      {stories.isLoading ? (
        <p>Loading...</p>
      ) : (
        {/* 传递常规数据 */}
        <List list={stories.data} onRemoveItem={handleRemoveStory} />
      )}
    </>
  );
};
复制代码


searchTerm 就是输入框输入的值:


  • 初始值为 React,组件加载时
  • 值为空,就什么也不做
  • 每次值改变时,都执行副作用函数获取相关数据


也就是说我们【每次在输入框输入内容都会重新获取一次数据】,这个功能完全在服务端完成,但这样频繁的调用 API 可能会导致第三方的限速,接口会返回错误。


一个简单的解决方式就是【加一个确认按钮】,当点击按钮时才会重新获取数据:


const App = () => {
  const [searchTerm, setSearchTerm] = useSemiPersistentState("search", "React");
  const [url, setUrl] = React.useState(`${API_ENDPOINT}${searchTerm}`);
  ...
  React.useEffect(() => {
    // 删掉 if (searchTerm === "") return;
    dispatchStories({ type: "STORIES_FETCH_INIT" });
    fetch(url)
      ...
  }, [url]);
  ...
  const handleSearch = (e) => {
    setSearchTerm(e.target.value);
  };
  // 点击按钮的处理函数
  const handleSearchSubmit = () => {
    setUrl(`${API_ENDPOINT}${searchTerm}`);
  };
  return (
    <>
      ...
      {/* 增加一个按钮 */}
      <button disabled={!searchTerm} onClick={handleSearchSubmit}>
        Submit
      </button>
      <hr />
      ...
    </>
  );
};
复制代码


现在 searchTerm 仅用于更新输入框的值,url 代替了它获取数据的功能,当用户点击了提交按钮,url 会更新并调用副作用函数获取新的数据。


我们还可以通过 useCallback hook 进行优化(可跳过):


const App = () => {
  ...
  const handleFetchStories = React.useCallback(() => {
    dispatchStories({ type: "STORIES_FETCH_INIT" });
    fetch(url)
      .then((response) => response.json())
      .then((result) => {
        dispatchStories({
          type: "STORIES_FETCH_SUCCESS",
          payload: result.hits,
        });
      })
      .catch(() => dispatchStories({ type: "STORIES_FETCH_FAILURE" }));
  }, [url]);
  React.useEffect(() => {
    handleFetchStories();
  }, [handleFetchStories]);
  ...
};
复制代码


axios


原生浏览器提供的 fetch API 并不适用于所有情况,尤其是对于一些老版本的浏览器,所以我们决定使用一个稳定的库 axios 来替代 fetch API 完成异步数据的获取。


  • 首先通过 npm 安装:


npm install axios
复制代码
  • 引入到 App 文件中:


import axios from 'axios';
复制代码
  • 使用 axios 代替 fetch:


const handleFetchStories = React.useCallback(() => {
  dispatchStories({ type: "STORIES_FETCH_INIT" });
  axios.get(url)
    .then((result) => {
    dispatchStories({
      type: "STORIES_FETCH_SUCCESS",
      payload: result.data.hits,    // 注意
    });
  })
    .catch(() => dispatchStories({ type: "STORIES_FETCH_FAILURE" }));
}, [url]);
复制代码

与 fetch 相似,它将 url 作为参数并返回一个 promise 对象,同时因为它把结果包装成了 JS 的数据对象,所以并不需要将返回的结果转换为 JSON。


表单


表单在 React 和 HTML 中并没有太大区别,我们只需要将 handleSearchSubmit() 绑定在 form 元素上,再把按钮的 type 属性设置为 submit 就好了:


const App = () => {
  ...
  return (
    <>
      <form onsubmit={handleSearchSubmit}>
        <InputWithLabel
          id="search"
          value={searchTerm}
          onInputChange={handleSearch}
          isFocused
        >
          <strong>Search:</strong>
        </InputWithLabel>
        <button disabled={!searchTerm} type="submit">
          Submit
        </button>
      </form>
      <hr />
      ...
    </>
  );
};
复制代码

这样我们就可以使用  Enter 键进行搜索了,也别忘了阻止浏览器刷新:


const handleSearchSubmit = (e) => {
  e.preventDefault();
  setUrl(`${API_ENDPOINT}${searchTerm}`);
};
复制代码

继续将 form 提取为独立的 SearchForm 组件,同样引入到 App 组件中:


const App = () => {
  ...
  return (
    <>
      <h1>Hacker Stories</h1>
      <SearchForm
        searchTerm={searchTerm}
        onSearch={handleSearch}
        onSearchSubmit={handleSearchSubmit}
      />
      <hr />
      ...
    </>
  );
};
const SearchForm = ({ searchTerm, onSearch, onSearchSubmit }) => (
  <form onsubmit={onSearchSubmit}>
    <InputWithLabel
      id="search"
      value={searchTerm}
      onInputChange={onSearch}
      isFocused
    >
      <strong>Search:</strong>
    </InputWithLabel>
    <button disabled={!searchTerm} type="submit" className="btn">
      Submit
    </button>
  </form>
);
复制代码

样式


React 中写样式的方法有很多种,我们这里只讨论最常见的 CSS 样式,与标准 CSS 的 class 属性类似,React 为每个元素都提供了一个 className 属性,可以通过它在 CSS 文件中设置样式。


...
  return (
    <div className="container">
      <h1 className="headline">Hacker Stories</h1>
      <SearchForm
        searchTerm={searchTerm}
        onSearch={handleSearch}
        onSearchSubmit={handleSearchSubmit}
      />
      {stories.isError && <p>Something went wrong ...</p>}
      {stories.isLoading ? (
        <p>Loading...</p>
      ) : (
        <List list={stories.data} onRemoveItem={handleRemoveStory} />
      )}
    </div>
  );
};
复制代码

因为我们使用了 create-react-app 来创建应用,所以你会看到 src/App.css 文件以及它的导入语句:


import './App.css';
复制代码

像这样修改它们的样式,你也可以仿照我写的代码


body {
  background: linear-gradient(to left, #b6fbff, #83a4d4);
  color: #171212;
}
.container {
  padding: 20px;
}
.headline {
  font-size: 48px;
  letter-spacing: 2px;
}
复制代码


我们的教程到这差不多结束了,之后就需要你自行探索了😎

有几个方向可以供你学习:


  • 配置 Sass
  • CSS Modules
  • CSS in JS
  • 部署应用
  • React 性能
  • TypeScript
  • 测试
  • ...
目录
相关文章
|
前端开发 JavaScript API
2022 React 最速上手指南(十二)—— children prop & 指令式 React
2022 React 最速上手指南(十二)—— children prop & 指令式 React
242 0
2022 React 最速上手指南(十二)—— children prop & 指令式 React
|
前端开发 API
2022 React 最速上手指南(九)—— 受控组件 & 单向数据流
2022 React 最速上手指南(九)—— 受控组件 & 单向数据流
177 0
2022 React 最速上手指南(九)—— 受控组件 & 单向数据流
|
前端开发 JavaScript
2022 React 最速上手指南(八)—— 状态提升 & React fragment
2022 React 最速上手指南(八)—— 状态提升 & React fragment
208 0
2022 React 最速上手指南(八)—— 状态提升 & React fragment
|
缓存 前端开发 JavaScript
2022 React 最速上手指南(十三)—— 内联处理函数 & 异步数据 & 条件渲染
2022 React 最速上手指南(十三)—— 内联处理函数 & 异步数据 & 条件渲染
283 0
|
存储 前端开发
2022 React 最速上手指南(十一)—— 自定义 hook & 可复用组件
2022 React 最速上手指南(十一)—— 自定义 hook & 可复用组件
250 0
|
前端开发 JavaScript API
2022 React 最速上手指南(十)—— props 处理 & React 副作用
2022 React 最速上手指南(十)—— props 处理 & React 副作用
264 0
|
前端开发
2022 React 最速上手指南(七)—— React state & 回调处理函数
2022 React 最速上手指南(七)—— React state & 回调处理函数
233 0
|
前端开发 JavaScript 开发者
2022 React 最速上手指南(六)—— JSX 处理函数 & React props
2022 React 最速上手指南(六)—— JSX 处理函数 & React props
145 0
|
前端开发 JavaScript
2022 React 最速上手指南(五)—— 再谈组件 & 导入导出
2022 React 最速上手指南(五)—— 再谈组件 & 导入导出
428 0
|
6月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
341 0