Heim  >  Artikel  >  Web-Frontend  >  Die Entwicklung des React State Management: Von lokal zu asynchron

Die Entwicklung des React State Management: Von lokal zu asynchron

WBOY
WBOYOriginal
2024-08-21 06:49:021135Durchsuche

Inhaltsverzeichnis

  • Einführung
  • Lokaler Staat
    • Klassenkomponenten
    • Funktionskomponenten
    • Verwenden Sie den Reduzierhaken
  • Globaler Staat
    • Was ist ein globaler Staat?
    • Wie benutzt man es?
    • Der Hauptweg
    • Der einfache Weg
    • Der falsche Weg
  • Asynchroner Status
  • Fazit

Einführung

Hallo!

Dieser Artikel bietet einen Überblick darüber, wie Zustand in React Applications vor Tausenden von Jahren verwaltet wurde, als Klassenkomponenten die Welt dominierten und Funktionskomponenten bis vor Kurzem nur eine mutige Idee waren , wenn ein neues Paradigma des Zustands entstanden ist: Asynchroner Zustand.

Lokaler Staat

Okay, jeder, der bereits mit React gearbeitet hat, weiß, was ein Local State ist.

Ich weiß nicht, was es ist
Der lokale Status ist der Status einer einzelnen Komponente.

Jedes Mal, wenn ein Status aktualisiert wird, wird die Komponente neu gerendert.


Möglicherweise haben Sie mit dieser alten Struktur gearbeitet:

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>;
  }
}
}

Vielleicht ein modernes funktionales eines:

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>;
};

Oder sogar ein „akzeptierteres“? (Aber definitiv seltener)

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

Was Sie zum Staunen bringen kann...

Warum zum Hack sollte ich diesen komplexen Reduzierer für eine einzelne Komponente schreiben?

Nun, React hat diesen hässlichen Hook namens useReducer von einem sehr wichtigen Tool namens Redux geerbt.

Wenn Sie jemals mit Global State Management in React zu tun hatten, müssen Sie von Redux gehört haben.

Das bringt uns zum nächsten Thema: Global State Management.

Globaler Staat

Global State Management ist eines der ersten komplexen Themen beim Erlernen von React.

Was ist das?

Es können mehrere Dinge sein, die auf viele Arten erstellt werden und unterschiedliche Bibliotheken haben.

Ich definiere es gerne als:

Ein einzelnes JSON-Objekt, auf das von jeder Komponente der Anwendung zugegriffen und das es verwaltet wird.

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

Ich stelle es mir gerne so vor:

Eine Front-End-No-SQL-Datenbank.

Das ist richtig, eine Datenbank. Hier speichern Sie Anwendungsdaten, die Ihre Komponenten lesen/schreiben/aktualisieren/löschen können.

Ich weiß, dass der Status standardmäßig jedes Mal neu erstellt wird, wenn der Benutzer die Seite neu lädt, aber das ist möglicherweise nicht das, was Sie möchten, und wenn Sie Daten irgendwo speichern (z. B. im localStorage), möchten Sie das vielleicht um mehr über Migrationen zu erfahren, um zu vermeiden, dass die App bei jeder neuen Bereitstellung kaputt geht.

Ich verwende es gerne als:

Ein mehrdimensionales Portal, in dem Komponenten ihre Gefühle mitteilen und ihre Attribute auswählen können. Alles, überall, alles auf einmal.

Wie benutzt man es?

Der Hauptweg

Redux

Es ist der Industriestandard.

Ich arbeite seit 7 Jahren mit React, TypeScript und Redux. Jedes Projekt, an dem ich professionell gearbeitet habe, verwendet Redux.

Die überwiegende Mehrheit der Leute, die ich getroffen habe und die mit React arbeiten, verwenden Redux.

Das am häufigsten genannte Tool in React offene Stellen bei Trampar de Casa ist Redux.

Das beliebteste React State Management-Tool ist...

The Evolution of React State Management: From Local to Async

Redux

The Evolution of React State Management: From Local to Async

Wenn Sie mit React arbeiten möchten, sollten Sie Redux lernen.
Wenn Sie derzeit mit React arbeiten, wissen Sie es wahrscheinlich bereits.

Ok, so rufen wir normalerweise Daten mit Redux ab.

Haftungsausschluss
„Was? Macht das Sinn? Redux dient zum Speichern von Daten, nicht zum Abrufen. Wie zum Teufel würden Sie Daten mit Redux abrufen?“

Wenn Sie darüber nachgedacht haben, muss ich Ihnen Folgendes sagen:

Ich rufe eigentlich keine Daten mit Redux ab.
Redux wird das Kabinett für die Anwendung sein, es speichert ~shoes~-Zustände, die in direktem Zusammenhang mit dem Abrufen stehen, deshalb habe ich diesen falschen Ausdruck verwendet: „Daten mit Redux abrufen“.


// 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;
  }
};

Jetzt integrieren wir auf der UI-Seite Aktionen mit useDispatch und 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>
  );
};

Wenn Commits.tsx die einzige Komponente war, die auf die Commits-Liste zugreifen musste, sollten Sie diese Daten nicht im globalen Status speichern. Stattdessen könnte der örtliche Staat genutzt werden.

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

Für diesen Artikel verwende ich React-Query.

Ich empfehle Ihnen, auf die Dokumentation jedes dieser Tools zuzugreifen, um besser zu verstehen, welche Probleme sie lösen.

Hier ist der Code:

Keine Aktionen mehr, keine Versendungen mehr, kein Global State

mehr zum Abrufen von Daten.

Das müssen Sie in Ihrer App.tsx-Datei tun, damit React-Query richtig konfiguriert ist:

Sie sehen, Async State

ist etwas Besonderes.

Es ist wie bei Schrödingers Katze – man kennt den Zustand erst, wenn man ihn beobachtet (oder laufen lässt).

Aber Moment: Wenn beide Komponenten useCommits aufrufen und useCommits einen API-Endpunkt aufruft, bedeutet das dann, dass es ZWEI identische Anforderungen zum Laden derselben Daten gibt?

Kurze Antwort: Nein!

Lange Antwort: React Query ist großartig. Es erledigt diese Situation automatisch für Sie und verfügt über vorkonfiguriertes Caching, das intelligent genug ist, um zu wissen, wann Sie Ihre Daten neu abrufen

oder einfach den Cache verwenden müssen.

Es ist außerdem äußerst konfigurierbar, sodass Sie es an 100 % der Anforderungen Ihrer Anwendung anpassen können.

Jetzt haben wir unsere Komponenten immer bereit für isLoading oder isError und wir sorgen dafür, dass der Global State weniger verschmutzt wird und verfügen über einige ziemlich nette Funktionen, die sofort einsatzbereit sind.

Abschluss

Jetzt kennen Sie den Unterschied zwischen Lokal, Global und Asynchroner Status

.


Lokal -> Nur Komponente.
Global -> Single-Json-NoSQL-DB-For-The-FE.

Asynchron -> Externe Daten, Schrödingers katzenartige, außerhalb der FE-Anwendung lebende Daten, die geladen werden müssen und Fehler zurückgeben können.

Ich hoffe, Ihnen hat dieser Artikel gefallen. Lassen Sie mich wissen, wenn Sie anderer Meinung sind oder konstruktives Feedback haben. Prost!

Das obige ist der detaillierte Inhalt vonDie Entwicklung des React State Management: Von lokal zu asynchron. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn