The Evolution of React State Management: From Local to Async

WBOY
Release: 2024-08-21 06:49:02
Original
970 people have browsed it

Table of Contents

  • Introduction
  • Local State
    • Class Components
    • Functional Components
    • useReducer Hook
  • Global State
    • What is Global State?
    • How to Use It?
    • The Main Way
    • The Simple Way
    • The Wrong Way
  • Async State
  • Conclusion

Introduction

Hi!

This article presents an overview of howStatewas managed in React Applications thousands of years ago when Class Components dominated the world andfunctional componentswere just a bold idea, until recent times, when a new paradigm ofStatehas emerged:Async State.

Local State

Alright, everyone who has already worked with React knows what a Local State is.

I don't know what it is
Local State is the state of a single Component.

Every time a state is updated, the component re-renders.


You may have worked with this ancient structure:

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 
Loading...
; if (error) return
Error: {error}
; return (

Commit List

    {commits.map(commit => (
  • {commit.commit.message}
  • ))}
); } } class TotalCommitsCount extends Component { render() { return
Total commits: {this.props.count}
; } } }
Copy after login

Perhaps amodernfunctionalone:

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 
Loading...
; if (error) return
Error: {error}
; return (

Commit List

    {commits.map(commit => (
  • {commit.commit.message}
  • ))}
); }; const TotalCommitsCount = ({ count }) => { return
Total commits: {count}
; };
Copy after login

Or even a"more accepted"one? (Definitely more rare though)

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' }); };
Copy after login

Which can make you wonder...

Why thehackwould I be writing this complex reducer for a single component?

Well, React inherited thisuglyhook called useReducer from a very important tool calledRedux.

If you ever had to deal withGlobal State Managementin React, you must've heard aboutRedux.

This brings us to the next topic: Global State Management.

Global State

Global State Management is one of the first complex subjects when learning React.

What is it?

It can be multiple things, built in many ways, with different libraries.

I like to define it as:

A single JSON object, accessed and maintained by any Component of the application.

const globalState = { isUnique: true, isAccessible: true, isModifiable: true, isFEOnly: true }
Copy after login

I like to think of it as:

A Front-EndNo-SQLDatabase.

That's right, a Database. It's where you store application data, that your components can read/write/update/delete.

I know, by default, the state will be recreated whenever the user reloads the page, but that may not be what you want it to do, and if you're persisting data somewhere (like the localStorage), you might want to learn about migrations to avoid breaking the app every new deployment.

I like to use it as:

A multidimensional portal, where components candispatchtheir feelings andselecttheir attributes. Everything, everywhere, all at once.

How to use it?

The main way

Redux

It is the industry standard.

I have worked with React, TypeScript, andReduxfor 7 years. Every project I've worked withprofessionallyusesRedux.

The vast majority of people I've met who works with React, useRedux.

The most mentioned tool in React open positions at Trampar de Casa isRedux.

The most popular React State Management tool is...

The Evolution of React State Management: From Local to Async

Redux

The Evolution of React State Management: From Local to Async

If you want to work with React, you should learnRedux.
If you currently work withReact, you probably already know.

Ok, here's how we usually fetch data usingRedux.

Disclaimer
"What? Does this make sense? Redux is to store data, not to fetch, how the F would you fetch data with Redux?"

If you thought about this, I must tell you:

I'm not actuallyfetchingdata with Redux.
Redux will be the cabinet for the application, it'll store ~shoes~ states that are directly related tofetching, that's why I used this wrong phrase: "fetch data using 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; } };
Copy after login

Now on the UI side, we integrate with actions usinguseDispatchanduseSelector:

// 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 
Loading...
; if (isError) return
Error while trying to fetch commits.
; return (
    {commits.map(commit => (
  • {commit.commit.message}
  • ))}
); };
Copy after login

If Commits.tsx was the only component that needed to access commits list, you shouldn't store this data on the Global State. It could use the local state instead.

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 
Total commits: {commitCount}
; }
Copy after login

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 = () => ( 
) const store = createStore(rootReducer, applyMiddleware(thunk)); ReactDOM.render( , document.getElementById('root') );
Copy after login

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.

The simple 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,TWOopen Pull Requests.

ONE OF THEM IS ABOUT UPDATING IT'S DOC
The Evolution of React State Management: From Local to Async

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 }); } }, }));
Copy after login

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 
Loading...
; if (isError) return
Error occurred
; return (
    {commits.map(commit => (
  • {commit.commit.message}
  • ))}
); }
Copy after login

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 ( 

Total Commits:

{totalCommits}

); };
Copy after login

There are no actions and reducers, there is a Store.

And it's advisable to have slices of Store, so everything is near to thefeaturerelated to the data.

It works perfect with a folder-by-feature folder structure.
The Evolution of React State Management: From Local to Async

The wrong way

I need to confess something, both of my previous examples are wrong.

And let me do a quick disclaimer: They're notwrong, 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:
The Evolution of React State Management: From Local to Async

What happens if I need to fetch data from 20 endpoints inside 20 components?

  • 20x isLoading + 20x isError + 20x actions to mutate this properties.

What will they look like?

With 20 endpoints, this will become averyrepetitive 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 intoa lot of workforbasicfeatures (like caching) and well-written components that are prepared for loading/error states.

This is whyAsync Statewas born.

Async State

Before talking about Async State I want to mention something. We knowhowto use Local and Global state but at this time I didn't mentionwhatshould be stored andwhy.

TheGlobal Stateexample 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 theTotal Commits Countis 0, so presenting this value is presentinga lie.

In fact, until the request finishes, there is no way to know for sure what's theTotal Commits Countvalue.

This is because theTotal Commits Countis not a value we haveinsidethe application. It'sexternalinformation,asyncstuff, you know.

We shouldn't be telling lies if we don't know the truth.

That's why we must identifyAsync Statein our application and create components prepared for it.

We can do this with React-Query, SWR, Redux Toolkit Query and many others.

The Evolution of React State Management: From Local to Async

この記事では React-Query を使用します。

どの問題が解決されるかをよりよく理解するために、これらの各ツールのドキュメントにアクセスすることをお勧めします。

コードは次のとおりです:

データを取得するためのアクション、ディスパッチ、グローバル状態はもう必要ありません。

React-Query を適切に構成するには、App.tsx ファイルで次のことを行う必要があります:

ご存知のとおり、

非同期状態は特別です。

それはシュレーディンガーの猫のようなものです – 観察する (または実行する) まで状態はわかりません。

しかし、ちょっと待ってください。両方のコンポーネントが useCommits を呼び出し、useCommits が API エンドポイントを呼び出している場合、これは、同じデータをロードするために 2 つの同一のリクエストが存在することを意味しますか?

短い答え:

いいえ!

長い答え: React Query は素晴らしいです。この状況は自動的に処理され、データを

再フェッチするタイミング、または単純にキャッシュを使用するタイミングを認識するのに十分な事前構成済みのキャッシュが付属しています。

また、非常に構成可能であるため、アプリケーションのニーズに 100% 適合するように調整できます。

これで、コンポーネントは常に isLoading または isError に対応できるようになり、グローバル状態の汚染が少なくなり、すぐに使用できる非常に優れた機能がいくつか追加されました。

結論

これで、

ローカル

グローバル非同期状態の違いが分かりました。ローカル ->コンポーネントのみ。グローバル -> Single-Json-NoSQL-DB-For-The-FE.非同期 ->外部データ、シュレディンガーの猫のようなもの。FE アプリケーションの外側に存在し、読み込みを必要とし、エラーを返す可能性があります。

この記事を楽しんでいただければ幸いです。異なる意見や建設的なフィードバックがありましたら、お知らせください。乾杯!

The above is the detailed content of The Evolution of React State Management: From Local to Async. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!