こんにちは!
この記事では、クラス コンポーネントが世界を支配し、機能コンポーネント が最近まで大胆なアイデアにすぎなかった数千年前に、React アプリケーションで 状態 がどのように管理されていたかについて概要を説明します。 状態の新しいパラダイムが出現したとき: 非同期状態.
はい、すでに React を使ったことがある人なら誰でも、ローカル状態が何であるかを知っています。
状態が更新されるたびに、コンポーネントは再レンダリングされます。それが何なのか分かりません
ローカル状態は、単一のコンポーネントの状態です。
あなたはこの古代の構造物を扱ったことがあるかもしれません:
class CommitList extends React.Component { constructor(props) { super(props); this.state = { isLoading: false, commits: [], error: null }; } componentDidMount() { this.fetchCommits(); } fetchCommits = async () => { this.setState({ isLoading: true }); try { const response = await fetch('https://api.github.com/repos/facebook/react/commits'); const data = await response.json(); this.setState({ commits: data, isLoading: false }); } catch (error) { this.setState({ error: error.message, isLoading: false }); } }; render() { const { isLoading, commits, error } = this.state; if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <h2>Commit List</h2> <ul> {commits.map(commit => ( <li key={commit.sha}>{commit.commit.message}</li> ))} </ul> <TotalCommitsCount count={commits.length} /> </div> ); } } class TotalCommitsCount extends Component { render() { return <div>Total commits: {this.props.count}</div>; } } }
おそらくモダン 機能的 のもの:
const CommitList = () => { const [isLoading, setIsLoading] = useState(false); const [commits, setCommits] = useState([]); const [error, setError] = useState(null); // To update state you can use setIsLoading, setCommits or setUsername. // As each function will overwrite only the state bound to it. // NOTE: It will still cause a full-component re-render useEffect(() => { const fetchCommits = async () => { setIsLoading(true); try { const response = await fetch('https://api.github.com/repos/facebook/react/commits'); const data = await response.json(); setCommits(data); setIsLoading(false); } catch (error) { setError(error.message); setIsLoading(false); } }; fetchCommits(); }, []); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <h2>Commit List</h2> <ul> {commits.map(commit => ( <li key={commit.sha}>{commit.commit.message}</li> ))} </ul> <TotalCommitsCount count={commits.length} /> </div> ); }; const TotalCommitsCount = ({ count }) => { return <div>Total commits: {count}</div>; };
それとも「もっと受け入れられる」ものでしょうか? (間違いなくもっと珍しいですが)
const initialState = { isLoading: false, commits: [], userName: '' }; const reducer = (state, action) => { switch (action.type) { case 'SET_LOADING': return { ...state, isLoading: action.payload }; case 'SET_COMMITS': return { ...state, commits: action.payload }; case 'SET_USERNAME': return { ...state, userName: action.payload }; default: return state; } }; const CommitList = () => { const [state, dispatch] = useReducer(reducer, initialState); const { isLoading, commits, userName } = state; // To update state, use dispatch. For example: // dispatch({ type: 'SET_LOADING', payload: true }); // dispatch({ type: 'SET_COMMITS', payload: [...] }); // dispatch({ type: 'SET_USERNAME', payload: 'newUsername' }); };
それは疑問に思うかもしれません...
単一コンポーネントに対してこの複雑なリデューサーを作成する必要があるのはなぜですか?ハック
そうですね、React は、Redux という非常に重要なツールから useReducer と呼ばれるこの 醜い フックを継承しました。
React でグローバル ステート管理 を扱う必要があった場合は、Redux について聞いたことがあるはずです。
これで次のトピック、グローバル状態管理に移ります。世界の状態
それは何ですか?
私はそれを次のように定義したいと思います:
アプリケーションのコンポーネントによってアクセスおよび維持される単一の JSON オブジェクト。
const globalState = { isUnique: true, isAccessible: true, isModifiable: true, isFEOnly: true }
私は次のように考えるのが好きです:
フロントエンドのそうです、データベースです。ここには、コンポーネントが読み取り/書き込み/更新/削除できるアプリケーション データが保存されます。SQL を使用しない データベース。
デフォルトでは、ユーザーがページをリロードするたびに状態が再作成されることはわかっていますが、それは望んでいることではない可能性があり、データをどこか (localStorage など) に永続化している場合は、次のようにする必要があるかもしれません。新しくデプロイするたびにアプリが壊れることを避けるための移行について学びます。
私は次のように使用するのが好きです:
コンポーネントが感情を発信し、属性を選択できる多次元ポータル。すべてを、どこでも、一度に。
リダックス
これは業界標準です。
私は React、TypeScript、および Redux を 7 年間使用してきました。私が専門的にと一緒に取り組んだすべてのプロジェクトはReduxを使用しています。
私が会った React を使っている人の大多数は Redux を使用しています。
Trampar de Casa の React 募集ポジションで最も言及されているツールは Redux です。
最も人気のある React 状態管理ツールは...
Redux
React を使って作業したい場合は、Redux を学ぶ必要があります。
現在 React を使用している場合は、おそらくすでにご存知でしょう。
わかりました。Redux を使用して通常どのようにデータを取得するかを示します。
これについて考えたことがあるなら、こう言わなければなりません: 実際には Redux でデータをフェッチしていません。 免責事項
「何ですか? これには意味がありますか? Redux はデータを保存するものであり、フェッチするものではありません。Redux を使用してデータをフェッチするにはどうすればよいでしょうか?」
Redux はアプリケーションのキャビネットになります。フェッチに直接関連する ~shoes~ 状態を保存します。それが、私がこの間違ったフレーズを使用した理由です。「Redux を使用してデータをフェッチする」
。
// actions export const SET_LOADING = 'SET_LOADING'; export const setLoading = (isLoading) => ({ type: SET_LOADING, payload: isLoading, }); export const SET_ERROR = 'SET_ERROR'; export const setError = (isError) => ({ type: SET_ERROR, payload: isError, }); export const SET_COMMITS = 'SET_COMMITS'; export const setCommits = (commits) => ({ type: SET_COMMITS, payload: commits, }); // To be able to use ASYNC action, it's required to use redux-thunk as a middleware export const fetchCommits = () => async (dispatch) => { dispatch(setLoading(true)); try { const response = await fetch('https://api.github.com/repos/facebook/react/commits'); const data = await response.json(); dispatch(setCommits(data)); dispatch(setError(false)); } catch (error) { dispatch(setError(true)); } finally { dispatch(setLoading(false)); } }; // the state shared between 2-to-many components const initialState = { isLoading: false, isError: false, commits: [], }; // reducer export const rootReducer = (state = initialState, action) => { // This could also be actions[action.type]. switch (action.type) { case SET_LOADING: return { ...state, isLoading: action.payload }; case SET_ERROR: return { ...state, isError: action.payload }; case SET_COMMITS: return { ...state, commits: action.payload }; default: return state; } };
UI 側では、useDispatch と useSelector:
を使用してアクションと統合します。
// Commits.tsx import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchCommits } from './action'; export const Commits = () => { const dispatch = useDispatch(); const { isLoading, isError, commits } = useSelector(state => state); useEffect(() => { dispatch(fetchCommits()); }, [dispatch]); if (isLoading) return <div>Loading...</div>; if (isError) return <div>Error while trying to fetch commits.</div>; return ( <ul> {commits.map(commit => ( <li key={commit.sha}>{commit.commit.message}</li> ))} </ul> ); };
Commits.tsx がコミット リストにアクセスする必要がある唯一のコンポーネントである場合、このデータをグローバル状態に保存すべきではありません。代わりにローカル状態を使用することもできます。
But let's suppose you have other components that need to interact with this list, one of them may be as simple as this one:
// TotalCommitsCount.tsx import React from 'react'; import { useSelector } from 'react-redux'; export const TotalCommitsCount = () => { const commitCount = useSelector(state => state.commits.length); return <div>Total commits: {commitCount}</div>; }
Disclaimer
In theory, this piece of code would make more sense living inside Commits.tsx, but let's assume we want to display this component in multiple places of the app and it makes sense to put the commits list on the Global State and to have this TotalCommitsCount component.
With the index.js component being something like this:
import React from 'react'; import ReactDOM from 'react-dom'; import thunk from 'redux-thunk'; import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; import { Commits } from "./Commits" import { TotalCommitsCount } from "./TotalCommitsCount" export const App = () => ( <main> <TotalCommitsCount /> <Commits /> </main> ) const store = createStore(rootReducer, applyMiddleware(thunk)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
This works, but man, that looks overly complicated for something as simple as fetching data right?
Redux feels a little too bloated to me.
You're forced to create actions and reducers, often also need to create a string name for the action to be used inside the reducer, and depending on the folder structure of the project, each layer could be in a different file.
Which is not productive.
But wait, there is a simpler way.
Zustand
At the time I'm writing this article, Zustand has 3,495,826 million weekly downloads, more than 45,000 stars on GitHub, and 2, that's right, TWO open Pull Requests.
ONE OF THEM IS ABOUT UPDATING IT'S DOC
If this is not a piece of Software Programming art, I don't know what it is.
Here's how to replicate the previous code using Zustand.
// store.js import create from 'zustand'; const useStore = create((set) => ({ isLoading: false, isError: false, commits: [], fetchCommits: async () => { set({ isLoading: true }); try { const response = await fetch('https://api.github.com/repos/facebook/react/commits'); const data = await response.json(); set({ commits: data, isError: false }); } catch (error) { set({ isError: true }); } finally { set({ isLoading: false }); } }, }));
This was our Store, now the UI.
// Commits.tsx import React, { useEffect } from 'react'; import useStore from './store'; export const Commits = () => { const { isLoading, isError, commits, fetchCommits } = useStore(); useEffect(() => { fetchCommits(); }, [fetchCommits]); if (isLoading) return <div>Loading...</div>; if (isError) return <div>Error occurred</div>; return ( <ul> {commits.map(commit => ( <li key={commit.sha}>{commit.commit.message}</li> ))} </ul> ); }
And last but not least.
// TotalCommitsCount.tsx import React from 'react'; import useStore from './store'; const TotalCommitsCount = () => { const totalCommits = useStore(state => state.commits.length); return ( <div> <h2>Total Commits:</h2> <p>{totalCommits}</p> </div> ); };
There are no actions and reducers, there is a Store.
And it's advisable to have slices of Store, so everything is near to the feature related to the data.
It works perfect with a folder-by-feature folder structure.
I need to confess something, both of my previous examples are wrong.
And let me do a quick disclaimer: They're not wrong, they're outdated, and therefore, wrong.
This wasn't always wrong though. That's how we used to develop data fetching in React applications a while ago, and you may still find code similar to this one out there in the world.
But there is another way.
An easier one, and more aligned with an essential feature for web development: Caching. But I'll get back to this subject later.
Currently, to fetch data in a single component, the following flow is required:
What happens if I need to fetch data from 20 endpoints inside 20 components?
What will they look like?
With 20 endpoints, this will become a very repetitive process and will cause a good amount of duplicated code.
What if you need to implement a caching feature to prevent recalling the same endpoint in a short period? (or any other condition)
Well, that will translate into a lot of work for basic features (like caching) and well-written components that are prepared for loading/error states.
This is why Async State was born.
Before talking about Async State I want to mention something. We know how to use Local and Global state but at this time I didn't mention what should be stored and why.
The Global State example has a flaw and an important one.
The TotalCommitsCount component will always display the Commits Count, even if it's loading or has an error.
If the request failed, there's no way to know that the Total Commits Count is 0, so presenting this value is presenting a lie.
In fact, until the request finishes, there is no way to know for sure what's the Total Commits Count value.
This is because the Total Commits Count is not a value we have inside the application. It's external information, async stuff, you know.
We shouldn't be telling lies if we don't know the truth.
That's why we must identify Async State in our application and create components prepared for it.
We can do this with React-Query, SWR, Redux Toolkit Query and many others.
この記事では、React-Query を使用します。
どの問題が解決されるのかをよりよく理解するために、これらの各ツールのドキュメントにアクセスすることをお勧めします。
コードは次のとおりです:
データを取得するためのアクション、ディスパッチ、グローバル状態
はもう必要ありません。React-Query を適切に構成するには、App.tsx ファイルで次のことを行う必要があります。
ご存知のように、非同期状態
は特別です。それはシュレディンガーの猫のようなものです。観察する (または実行する) まで状態はわかりません。
しかし、両方のコンポーネントが useCommits を呼び出しており、useCommits が API エンドポイントを呼び出している場合、これは、同じデータをロードするために 2 つの同一のリクエストが存在することを意味するのでしょうか?
短い答え: いいえ!
長い答え: React Query は素晴らしいです。この状況は自動的に処理され、データをいつ再フェッチ
するか、単にキャッシュを使用するかを判断できる、事前設定されたキャッシュが付属しています。また、非常に構成可能であるため、アプリケーションのニーズに 100% 適合するように調整できます。
これで、コンポーネントは常に isLoading または isError に対応できるようになり、グローバル状態の汚染が少なくなり、すぐに使用できる非常に優れた機能がいくつか追加されました。
これで、ローカル、グローバル、および 非同期状態
の違いが分かりました。
ローカル ->コンポーネントのみ。
グローバル -> Single-Json-NoSQL-DB-For-The-FE.
この記事を楽しんでいただければ幸いです。異なる意見や建設的なフィードバックがありましたら、ぜひお知らせください。
以上がReact 状態管理の進化: ローカルから非同期への詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。