首頁  >  文章  >  web前端  >  React 狀態管理的演進:從本地到非同步

React 狀態管理的演進:從本地到非同步

WBOY
WBOY原創
2024-08-21 06:49:021112瀏覽

目錄

  • 介紹
  • 當地州
    • 類別組件
    • 功能組件
    • 使用Reducer Hook
  • 全域狀態
    • 什麼是全局狀態?
    • 如何使用?
    • 主要途徑
    • 簡單的方法
    • 錯誤的方式
  • 非同步狀態
  • 結論

介紹

嗨!

本文概述了數千年前,當類組件統治世界時,狀態是如何在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

這將我們帶入下一個主題:全域狀態管理。

全域狀態

全域狀態管理是學習 React 時最先複雜的科目之一。

它是什麼?

它可以是多種東西,以多種方式構建,具有不同的庫。

我喜歡定義為:

單一 JSON 對象,由應用程式的任何元件存取和維護。

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

我喜歡將其視為:

前端No-SQL資料庫。

沒錯,就是資料庫。這是您儲存應用程式資料的地方,您的元件可以讀取/寫入/更新/刪除。

我知道,預設情況下,每當使用者重新載入頁面時都會重新建立狀態,但這可能不是您想要的,如果您在某個地方(例如localStorage)保存數據,您可能想要了解遷移以避免每次新部署都會破壞應用程式。

我喜歡用它作為:

一個多維門戶,組件可以發送他們的感受並選擇他們的屬性。一切、無所不在、同時發生。

如何使用?

主要方式

Redux

這是業界標準。

我使用 React、TypeScript 和 Redux 已經 7 年了。我專業參與過的每個專案都使用Redux

我見過的絕大多數使用 React 的人都使用 Redux

Trampar de Casa 的 React 空缺職位中提到最多的工具是 Redux

最受歡迎的 React 狀態管理工具是......

The Evolution of React State Management: From Local to Async

Redux

The Evolution of React State Management: From Local to Async

如果你想使用 React,你應該學習 Redux
如果您目前使用 React,您可能已經知道了。

好的,這就是我們通常使用 Redux 來取得資料的方式。

免責聲明
「啥?這有道理嗎?Redux 是儲存資料的,不是取資料的,你F怎麼用 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 方面,我們使用 useDispatchuseSelector:
與操作集成

// 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.

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, TWO open 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 });
    }
  },
}));

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.
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 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:
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 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.

Async State

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.

The Evolution of React State Management: From Local to Async

在本文中,我將使用 React-Query。

我建議您訪問每個工具的文檔,以便更好地了解它們解決的問題。

程式碼如下:

不再需要執行任何操作,不再需要調度,不再需要全域狀態

來取得資料。

這是您必須在 App.tsx 檔案中執行的操作,才能正確配置 React-Query:

你看,非同步狀態

很特別。

這就像薛定諤的貓 - 在觀察(或運行它)之前你不知道它的狀態。

但等等,如果兩個元件都在呼叫 useCommits 並且 useCommits 正在呼叫 API 端點,這是否意味著將會有兩個相同的請求來載入相同的資料?

簡短回答:不行!

長答案:React Query 非常棒。它會自動為您處理這種情況,它帶有預先配置的緩存,足夠智能,可以知道何時重新獲取

您的資料或簡單地使用快取。

它的可配置性也非常好,因此您可以進行調整以適應 100% 的應用程式需求。

現在我們的元件始終為 isLoading 或 isError 做好準備,並且我們可以減少全域狀態的污染,並且擁有一些非常簡潔的開箱即用功能。

結論

現在您知道本地全域非同步狀態

之間的差異。


本地->僅組件。
全球-> Single-Json-NoSQL-DB-For-The-FE。

非同步->外部數據,就像薛丁格的貓一樣,存在於 FE 應用程式之外,需要加載並且可能返回錯誤。

希望您喜歡這篇文章,如果您有不同意見或任何建設性回饋,請告訴我,乾杯!

以上是React 狀態管理的演進:從本地到非同步的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn