Ylösskaalaus reduktorin ja kontekstin avulla

Reduktorien avulla voit yhdistää komponentin tilan päivityslogiikan. Kontekstin avulla voit välittää tietoa syvälle muihin komponentteihin. Voit yhdistää reduktoreita ja konteksteja hallitaksesi monimutkaisen ruudun tilaa.

Tulet oppimaan

  • Miten yhdistää reduktori ja konteksti
  • Miten välttää tilan ja toimintojen välittäminen propsien kautta
  • Miten pitää konteksti ja tilalogiikka erillisessä tiedostossa

Reduktorin yhdistäminen kontekstin kanssa

Tässä esimerkissä reduktoriin tutustumisesta, tilaa hallitaan reduktorilla. Reduktorifunktio sisältää kaiken tilan päivityslogiikan ja se on määritelty tiedoston loppuun:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Day off in Kyoto</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

Reduktori helpottaa pitämään tapahtumakäsittelijät lyhyinä ja tiiviinä. Kuitenkin, kun sovelluksesi kasvaa, saataat törmätä toiseen haasteeseen. Tällä hetkellä, tasks tila ja dispatch funktio ovat vain saatavilla ylätason TaskApp komponentissa. Jotta muut komponentit voisivat lukea tehtävälistan tai muuttaa sitä, sinun on erikseen välitettävä nykyinen tila ja tapahtumakäsittelijät, jotka muuttavat sitä propsien kautta.

Esimerkiksi, TaskApp välittää tehtävälistan ja tapahtumakäsittelijät TaskList komponentille:

<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

And TaskList passes the event handlers to Task:

<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

Pienessä esimerkissä kuten tässä, tämä toimii hyvin, mutta jos sinulla on kymmeniä tai satoja komponentteja välissä, tilan ja funktioiden välittäminen voi olla melko ärsyttävää!

Tämän takia vaihtoehtoinen tapa on laittaa sekä tasks tila että dispatch funktio kontekstiin. Tällöin, minkä tahansa komponentin alla oleva komponentti voi lukea tehtävälistan ja kutsua tapahtumakäsittelijöitä ilman toistuvaa “prop drillingia”.

Tässä on tapa yhdistää reduktori kontekstiin:

  1. Luo konteksti.
  2. Aseta tila ja dispatch -funktio kontekstiin.
  3. Käytä kontekstia missä tahansa puun sijainnissa.

1. Vaihe: Luo konteksti

useReducer hookki palauttaa nykyisen tasks tilan ja dispatch funktion, joka mahdollistaa sen päivittämisen:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

Välittääksesi ne puun alle, sinun täytyy luoda kaksi erillistä kontekstia:

  • TasksContext tarjoaa nykyisen listan tehtävistä.
  • TasksDispatchContext tarjoaa funktion, jonka avulla voit lähettää toimintoja.

Exporttaa ne erillisestä tiedostosta, jotta voit myöhemmin importata ne muista tiedostoista:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

Tässä, välität null:n oleetusarvona molemmille konteksteille. Todelliset arvot tarjotaan TaskApp komponentissa.

2. Vaihe: Aseta tila ja dispatch kontekstiin

Nyt voit importata molemmat kontekstit TaskApp komponentissa. Ota useReducer() funktion palauttamat tasks ja dispatch ja tarjoa ne kaikille komponenteille jotka ovat TaskApp komponentin alapuolella:

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

Tällä hetkellä välität tiedon sekä propsien että kontekstin kautta:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        <h1>Day off in Kyoto</h1>
        <AddTask
          onAddTask={handleAddTask}
        />
        <TaskList
          tasks={tasks}
          onChangeTask={handleChangeTask}
          onDeleteTask={handleDeleteTask}
        />
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

Seuraavassa vaiheessa, tulet poistamaan propsien välittämisen.

3. Vaihe: Käytä kontekstia missä tahansa sijainnissa puuta

Nyt sinun ei tarvitse välittää listaa tehtävistä tai tapahtumankäsittelijöistä:

<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>

Sen sijaan, mikä tahansa komponentti, joka tarvitsee tehtävälistan, voi lukea sen TaskContext:sta:

export default function TaskList() {
const tasks = useContext(TasksContext);
// ...

Päivittääksesi listan, mikä tahansa komponentti voi lukea dispatch -funktion kontekstista ja kutsua sitä:

export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...

TaskApp komponentin ei tarvitse välittää yhtään tapahtumakäsittelijää, eikä TaskList komponentin tarvitse myöskään välittää yhtään tapahtumakäsittelijää Task komponentille. Jokainen komponentti lukee kontekstista sen, mitä se tarvitsee:

import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskList() {
  const tasks = useContext(TasksContext);
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useContext(TasksDispatchContext);
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

Tila silti “asuu” ylätason TaskApp komponentissa, jota hallinnoidaan useReducer hookilla. Mutta sen tasks ja dispatch ovat nyt saatavilla jokaiselle komponentille, kun konteksti importataan ja käytetään.

Koko virityksen siirtäminen yhteen tiedostoon

Sinun ei tarvitse tehdä tätä, mutta voit vielä siistiä komponentteja siirtämällä sekä reduktorin että kontekstin yhteen tiedostoon. Tällä hetkellä, TasksContext.js sisältää vain kaksi kontekstin määrittelyä:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

Tämä tiedosto tulee olemaan täynnä! Siirrät reduktorin yhden tiedoston sisään. Sitten määrität uuden TasksProvider komponentin samaan tiedostoon. Tämä komponentti yhdistää kaikki osat yhteen:

  1. Se hallitsee tilaa reduktorilla.
  2. Se tarjoaa molemmat kontekstit alla oleville komponenteille.
  3. Se ottaa children propsina, jotta voit välittää JSX:ää sille.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

Tämä postaa kaiken komplikaation ja virityksen pois TaskApp komponentista:

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Voit myös exportata funktioita, jotka käyttävät kontekstia TasksContext.js:stä:

export function useTasks() {
return useContext(TasksContext);
}

export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}

Kun komponentin tarvitsee lukea kontekstia, se voi tehdä sen näiden funktioiden kautta:

const tasks = useTasks();
const dispatch = useTasksDispatch();

Tämä ei muuta toiminnallisuutta mitenkään, mutta sen avulla voit myöhemmin erottaa näitä konteksteja enemmän tai lisätä logiikkaa funktioihin. Nyt kaikki kontekstin ja reduktorin viritys on TasksContext.js tiedostossa. Tämä pitää komponentit puhtaina ja selkeinä, keskittyen siihen, mitä ne näyttävät, eikä siihen, mistä ne saavat tiedot:

import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

Voit kuvitella TasksProvider-komponentin osaksi ruutua, joka osaa käsitellä tehtäviä, useTasks-funktion tapana lukea niitä ja useTasksDispatch-funktion tapana päivittää niitä mistä tahansa komponentista.

Huomaa

Funktiot kuten useTasks ja useTaskDispatch kutsutaan mukautetuiksi hookseiksi. Jos funktiosi nimi alkaa use-merkillä, se on hookki. Tämä mahdollistaa muun muassa useContext-hookin käytön niiden sisällä.

Kun sovelluksesi kasvaa, sinulla voi olla useita context-reducer-paria kuten tämä. Tämä on tehokas tapa skaalata sovellus ja nostaa tila ylös vähällä työllä, kun haluat lukea dataa syvältä puusta.

Kertaus

  • Voit yhdistää reduktorin ja kontekstin, jotta mikä tahansa komponentti voi lukea ja päivittää ylhäällä olevaa tilaa.
  • Tarjotaksesi tila ja dispatch -funktiot alla oleville komponenteille:
    1. Luo kaksi kontekstia (yksi tilalle, toinen dispatch -funktiolle).
    2. Tarjoa molemmat kontekstit komponentista joka käyttää reduktoria.
    3. Käytä jompaa kumpaa kontekstia komponenteista jotka tarvitsevat niitä.
  • Voit siistiä komponentteja vielä lisää siirtämällä kaiken virityksen yhteen tiedostoon.
    • Voit exportata komponentin kuten TasksProvider joka tarjoaa kontekstin.
    • Voit myös exportata mukautettuja hookkeja kuten useTasks ja useTasksDispatch.
  • Sinulla voi olla useita konteksti-reduktori -pareja sovelluksessasi.