以结果为导向,写给刚学完前端三剑客和想要了解 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.useReducer(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 没有更新,这显然是不合理的。
为了避免这种情况,我们可以把这些状态合并到同一个 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
- 测试
- …
专栏
因为参加打卡活动是每日更新,所以可能比较短小,可以关注一下 React 入门专栏。
在更新完后会整合为一整篇,感谢关注和点赞!