在页面刷新之前,React 上下文不会更新状态

React context does not update state until the page is refreshed

我有一份患者名单。我需要显示单个患者的数据。使用 id 唯一标识患者。当我使用 axios 从后端获取特定患者的数据时,它似乎工作得很好。但问题是,当我点击患者的 link 时,我需要状态更新到那个对象。

当我从 reducer.ts 文件登录时,负载完美加载。但是,当我点击 link 实际显示数据时,直到刷新页面才显示。但是,刷新后它确实将状态更改为我想要的唯一对象,然后恢复到初始状态。我希望它获得对象并将其保持在状态中。我哪里错了?

页面刷新前: 页面刷新后: index.ts:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { reducer, StateProvider } from "./state";

ReactDOM.render(
  <StateProvider reducer={reducer}>
    <App />
  </StateProvider>,
  document.getElementById('root')
);

Reducer.ts:

import { State } from "./state";
import { Patient } from "../types";

export type Action =
  | {
      type: "SET_PATIENT_LIST";
      payload: Patient[];
    }
  | {
      type: "SINGLE_PATIENT";
      payload: Patient;
    };

export const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "SET_PATIENT_LIST":
      return {
        ...state,
        patients: {
          ...action.payload.reduce(
            (memo, patient) => ({ ...memo, [patient.id]: patient }),
            {}
          ),
          ...state.patients
        }
      };
    case "SINGLE_PATIENT":
      console.log(action.payload); 
      return {
        ...state,
        patients: {
          ...state.patients,
          [action.payload.id]:  {
            ...state.patients[action.payload.id],
            ...action.payload,
          },
        }
      };
    default:
      return state;
  }
};

export const setPatientList = (patientList: Patient[]): Action => {
  return {
    type: "SET_PATIENT_LIST",
    payload: patientList
  };
};

export const setSinglePatient = (patient: Patient): Action => {
  return {
    type: "SINGLE_PATIENT",
    payload: patient
  };
};

State.tsx:

import React, { createContext, useContext, useReducer } from "react";
import { Patient } from "../types";
import { Action } from "./reducer";

export type State = {
  patients: { [id: string]: Patient };
};

const initialState: State = {
  patients: {}
};

export const StateContext = createContext<[State, React.Dispatch<Action>]>([initialState, () => initialState]);

type StateProviderProps = {
  reducer: React.Reducer<State, Action>;
  children: React.ReactElement;
};

export const StateProvider = ({
  reducer,
  children
}: StateProviderProps) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StateContext.Provider value={[state, dispatch]}>
      {children}
    </StateContext.Provider>
  );
};

export const useStateValue = () => useContext(StateContext);

App.tsx:

import React from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
import { Button, Divider, Container, Typography } from "@material-ui/core";

import { apiBaseUrl } from "./constants";
import { useStateValue, setPatientList } from "./state";
import { Patient } from "./types";

import PatientListPage from "./PatientListPage";
import SinglePatientPage from "./SinglePatientPage";

const App = () => {
  const [, dispatch] = useStateValue();
  React.useEffect(() => {
    void axios.get<void>(`${apiBaseUrl}/ping`);

    const fetchPatientList = async () => {
      try {
        const { data: patientListFromApi } = await axios.get<Patient[]>(
          `${apiBaseUrl}/patients`
        );
        dispatch(setPatientList(patientListFromApi));
      } catch (e) {
        console.error(e);
      }
    };

    void fetchPatientList();
  }, [dispatch]);


  return (
    <div className="App">
      <Router>
        <Container>
          <Typography variant="h3" style={{ marginBottom: "0.5em" }}>
            Patientor
          </Typography>
          <Button component={Link} to="/" variant="contained" color="primary">
            Home
          </Button>
          <Divider hidden />
          <Routes>
            <Route path="/" element={<PatientListPage />} />
            <Route path="/patients/:id" element={<SinglePatientPage />} />
          </Routes>
        </Container>
      </Router>
    </div>
  );
};

export default App;

PatientListPage.tsx:

import React from "react";
import axios from "axios";
import {
  Box,
  Table,
  Button,
  TableHead,
  Typography,
  TableCell,
  TableRow,
  TableBody
} from "@material-ui/core";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from 'react-router-dom';

const PatientListPage = () => {
  const [{ patients }, dispatch] = useStateValue();
  const [error, setError] = React.useState<string>();

  return (
    <div className="App">
      <Box>
        <Typography align="center" variant="h6">
          Patient list
        </Typography>
      </Box>
      <Table style={{ marginBottom: "1em" }}>
        <TableHead>
          <TableRow>
            <TableCell>Name</TableCell>
            <TableCell>Gender</TableCell>
            <TableCell>Occupation</TableCell>
            <TableCell>Health Rating</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {Object.values(patients).map((patient: Patient) => (
            <TableRow key={patient.id}>
              <TableCell>
                <Link to={`/patients/${patient.id}`}>
                  {patient.name}
                </Link>
              </TableCell>
              <TableCell>{patient.gender}</TableCell>
              <TableCell>{patient.occupation}</TableCell>
              <TableCell>
                <HealthRatingBar showText={false} rating={1} />
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
};

export default PatientListPage;

单个患者页面:

import React from "react";
import { Patient } from "../types";
import { useStateValue, setSinglePatient } from "../state";
import { useParams } from "react-router-dom";
import { Typography } from "@material-ui/core";
import { apiBaseUrl } from "../constants";
import axios from "axios";

const SinglePatientPage = () => {
  const [{ patients }, dispatch] = useStateValue();
  const { id } = useParams<{ id: string }>();
  React.useEffect(() => {
    const fetchSinglePatient = async () => {
      if(id !== undefined) {
        try {
          const { data: patientFromApi } = await axios.get<Patient>(
            `${apiBaseUrl}/patients/${id}`
          );
          dispatch(setSinglePatient(patientFromApi));
        } catch (e) {
          console.error(e);
        }
      }
    };
    void fetchSinglePatient();
  }, [dispatch]);

  if (patients) {
    console.log('inside singlepatientpage', patients);
     return (
       <div className="app">
         <Typography variant="h6" style={{ marginBottom: "0.5em" }}>
           {patient.name}
           <p>ssn: {patient.ssn}</p>
           <p>occupation: {patient.occupation}</p>
         </Typography>
       </div>
     );
  }

  return null;
};

export default SinglePatientPage;

似乎缺少 SinglePatientPage 组件,使用 id 路由参数作为获取特定患者记录的依赖项。

id 添加到 useEffect 挂钩的依赖数组,因为它在回调中被引用。

const SinglePatientPage = () => {
  const [{ patients }, dispatch] = useStateValue();
  const { id } = useParams<{ id: string }>();

  React.useEffect(() => {
    const fetchSinglePatient = async () => {
      if(id !== undefined) {
        try {
          const { data: patientFromApi } = await axios.get<Patient>(
            `${apiBaseUrl}/patients/${id}` // <-- id referenced here
          );
          dispatch(setSinglePatient(patientFromApi));
        } catch (e) {
          console.error(e);
        }
      }
    };
    void fetchSinglePatient();
  }, [dispatch, id]); // <-- add id dependency

  if (!patients) {
    return null;
  }

  return (
    <div className="app">
      <Typography variant="h6" style={{ marginBottom: "0.5em" }}>
        {patient.name}
        <p>ssn: {patient.ssn}</p>
        <p>occupation: {patient.occupation}</p>
      </Typography>
    </div>
  );
};

您可能会考虑将 React hooks eslint rules 添加到您的项目中,以帮助在将来捕获此类缺失的依赖项。

我知道问题出在哪里了。我没有为单个患者数据创建新状态。

export type State = {
  patients: { [id: string]: Patient };
  patient: Patient | null; //new state
};

const initialState: State = {
  patients: {},
  patient: null //new initial state
};

在减速器中,我正在 return 处理一组患者。现在我可以 return 单个患者的数据。

case "SINGLE_PATIENT":
  return {
    ...state,
    patient: action.payload // returning "patient" instead of "patients"
  };