React Data Grid: Использование крючков React для создания приложения Pomodoro

Авторский кредит: SHUHEB AHMED

В этом посте мы создадим приложение для повышения продуктивности, используя React Hooks и AG Grid. Мы расскажем о том, как React Hooks используются для создания этого приложения и конкретно AG Grid. Вы можете увидеть готовое приложение Pomodoro App в действии, размещенное здесь.

Вы можете скачать исходный код на Github и посмотреть, как построено приложение, чтобы лучше следить за обсуждением ниже.

Что такое React Hooks?

React Hooks – это функции, предоставляемые React, которые позволяют компонентам напрямую “подключаться” к функциям React (например, иметь переменную состояния, обращаться к контексту) без написания класса для этой цели. React Hooks следуют соглашению об именовании с префиксом use.

Приложение Pomodoro App использует следующие React Hooks:

Чтобы прочитать о React Hooks более подробно, посетите официальные документы React Docs и React Docs (beta).

Обзор исходного кода

Ниже приведен обзор структуры кодовой базы:

ag-grid-pomodoro
├── src
│ ├── components
│ │ ├── cell-renderers
│ │ │ ├── ActionCellRenderer.js
│ │ │ └── ProgressCellRenderer.js
│ │ ├── full-width-cell-renderers
│ │ │ └── AddTaskCellRenderer.js
│ │ ├── task-components
│ │ │ ├── TaskType.js
│ │ │ ├── TaskDetails.js
│ │ │ ├── TaskTimer.js
│ │ │ └── EndTime.js
│ │ ├── MainTask.js
│ │ ├── PomodoroGrid.js
│ │ └── SaveButton.js
│ ├── context
│ │ └── PomodoroContext.js
│ ├── reducers
│ │ └── reducers.js
│ ├── utils
│ │ ├── useTimer.js
│ │ └── date.js
│ ├── App.css
│ ├── App.js
│ └── index.js
├── README.md
└── package.json
Вход в полноэкранный режим Выход из полноэкранного режима

Код приложения находится в директории /src/. Ниже приведены ключевые файлы, содержащие важные компоненты приложения:

Обзор приложения

Теперь давайте рассмотрим, как работает приложение. Ниже представлен визуальный обзор пользовательского интерфейса приложения, показывающий три компонента (MainTask, PomodoroGrid и SaveButton), из которых оно состоит:


Обзор приложения

Компонент приложения определен, как показано ниже:

const App = () => {
  // [...]

  return (
    <>
      <PomodoroProvider>
        <MainTask />
        <PomodoroGrid />
        <SaveButton />
      </PomodoroProvider>
    </>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Состояние приложения хранится вне App и разделяется между его компонентами MainTask и PomodoroGrid.

Переменная state – это объект, который хранит массив tasks и activeTaskId для хранения ID задачи, которая в настоящее время активна, т.е. таймер был запущен для этой задачи. См. объявление переменной состояния ниже:

const state = {
    tasks: [],
    activeTaskId: -1
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вот диаграмма, показывающая, как это работает – обратите внимание, что MainTask и PomodoroGrid имеют доступ к общей переменной состояния, которую они оба могут читать и обновлять. Реализация состояния и то, как App взаимодействует с ним, рассматривается далее в разделе Управление состоянием с помощью useContext и useReducer.


Приложение может читать и обновлять общее состояние

Компонент MainTask

Этот компонент отображает группу кнопок для переключения между различными типами задач: pomodoro, короткий перерыв или длительный перерыв. Компонент также отображает таймер с кнопкой для переключения таймера. MainTask может читать из общего состояния, где хранятся задачи, так что если выбрана задача из PomodoroGrid, прогресс таймера и детали задачи этой задачи будут показаны внутри компонента MainTask.

Вы можете увидеть это в приведенном ниже GIF. Обратите внимание, что после нажатия кнопки “Старт” на задаче “Написать черновик блога” в сетке ниже, название задачи отображается в компоненте MainTask выше, а таймер начинает тикать:


Задача отображается внутри компонента MainTask

Компонент PomodoroGrid

PomodoroGrid отображает элемент AG Grid с каждой строкой внутри сетки, представляющей задачу. Подобно MainTask, компонент сетки может читать и обновлять общее состояние, где хранятся задачи, которое определено вне компонента PomodoroGrid.

Каждая строка сетки имеет три кнопки – (1) для переключения таймера, (2) для отметки задачи как выполненной и (3) для удаления задачи. Эти кнопки отображаются в столбце Action сетки.

Название задачи отображается в столбце Task.

В строке также отображается таймер в столбце Progress, ячейки которого отрисовываются с помощью ProgressCellRenderer.

В нижней части сетки есть прикрепленная строка, которая используется для добавления задач. Когда задача добавляется, состояние приложения (в котором хранятся все задачи) обновляется новой задачей, после чего сетка перерисовывается, отображая новую добавленную задачу.

Посмотрите на это в действии ниже:


Добавление задачи

Реализация этого процесса более подробно описана в следующем разделе.

Управление состоянием с помощью useContext и useReducer

Как упоминалось в предыдущих разделах, мы управляем состоянием вне PomodoroGrid и MainTask, чтобы оба компонента могли совместно использовать одни и те же данные и обновлять их при выполнении действия.

На диаграмме ниже показан обзор того, как происходит обмен и обновление состояния.


Обзор магазина

Следующие действия обновляют состояние:

  • Добавление задачи
  • завершение задачи
  • Переключение таймера задачи
  • удаление задачи

Чтобы обновить состояние на основе этих действий, мы используем хук useReducer, как описано ниже.

Reducer

Хук React useReducer позволяет вам обновлять текущее состояние путем диспетчеризации действий.

Редукторы – это чистые функции, которые получают текущее состояние приложения вместе с действием, которое должно быть выполнено над этим состоянием для получения нового состояния. По сути, редукторы можно представить как машину состояния, которая имеет некоторое начальное состояние и обновляет его на основе действия.

Вот пример того, как это можно определить:

const initialState = {
    tasks: [],
    activeTaskId: -1
};

const reducer = (state = {}, action) => {
    switch (action.type) {
        case 'added_task':
            return {
                ...state,
                tasks: [...state.tasks, {
                    id: action.id,
                    task: action.task,
                    taskNo: action.taskNo,
                    taskCount: action.taskCount,
                }]
            }
        // ...
        default:
            return state;
    }
}

function MyComponent() {
    const [state, dispatch] = useReducer(reducer, initialState);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как вы видите, хук useReducer возвращает кортеж из текущего state и метода dispatch, который используется для обновления состояния.

Действия

Действия описывают операцию, которую редуктор должен выполнить над состоянием. Например, действие для добавления новой задачи может выглядеть следующим образом:

const addTask = {
    type: 'added_task',
    id: generateId(),
    task: 'pick up groceries',
    taskNo: 1,
    taskCount: 1
 };
Войти в полноэкранный режим Выйти из полноэкранного режима

Используя метод dispatch, мы отправляем действие на reducer, который преобразует состояние.

В нашем приложении мы вызываем dispatch при нажатии на кнопку.

Вот код для диспетчеризации addTask, определенный выше:

function MyComponent() {
    const [state, dispatch] = useReducer(reducer, initialState);

    const addTask = {
    type: 'added_task',
    id: generateId(),
    task: 'pick up groceries',
    taskNo: 1,
    taskCount: 1
    };

    // this would be called from a button click
    const addTaskHandler = () => {
       dispatch(addTask);
    }
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Контекст

React Context позволяет вам обмениваться данными между компонентами React без необходимости вручную передавать их в качестве реквизитов каждому компоненту.

Чтобы поделиться state и dispatch в PomodoroGrid и MainTask, мы добавляем их в React Context, чтобы оба компонента могли обновлять состояние, когда это необходимо.

Контекст определяется следующим образом:

import { createContext } from 'react';

export const PomodoroContext = createContext();
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, когда мы создали PomodoroContext для хранения наших общих данных, следующим шагом будет создание компонента для обертывания приложения, который будет предоставлять контекст оттуда:

// src/context/PomodoroContext.js
import reducer from "../reducers/reducer";

// initial state
const gridState = {
    tasks: [],
    activeTaskId: -1
};

export const PomodoroProvider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, gridState);
    const { tasks, activeTaskId } = state;

    // [...]

    const value = {tasks, activeTaskId, dispatch}

    return (<PomodoroContext.Provider value={actions}>
        {children}
    </PomodoroContext.Provider>
    );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Компонент-обертка PomodoroProvider определяет хук useReducer для хранения state и метода dispatch. Компонент возвращает PomodoroContext.Provider и имеет свойство value, которое инициализирует PomodoroContext с task, activeTaskId и dispatch. В результате, любой компонент, который рендерится внутри PomodoroProvider, может получать tasks, activeTaskId и dispatch.

Компонент-обертка определяется вокруг всего приложения, что можно увидеть в приведенном ниже фрагменте. Обратите внимание, что MainTask, PomodoroGrid и SaveButton обернуты внутри PomodoroProvider, что означает, что они будут иметь доступ к tasks, activeTaskId и dispatch из PomodoroContext.

// src/App.js
import { PomodoroProvider } from './context/PomodoroContext';
import MainTask from './components/MainTask';
import SaveButton from './components/SaveButton';
import PomorodoGrid from './components/PomodoroGrid';

const App = () => {
  // [...]

  return (
    <>
      <PomodoroProvider>
        <MainTask />
        <PomodoroGrid />
        <SaveButton />
      </PomodoroProvider>
    </>
  );
}

export default App;

Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда компоненту нужен доступ к хранилищу, он может читать из PomodoroContext и получать tasks, activeTaskId и dispatch.

Например, компонент Grid может получить данные для отображения в виде строк из tasks. Ему не нужен доступ к dispatch или activeTaskId, поэтому они не извлекаются из контекста:

// src/components/PomodoroGrid.js
import React, { useContext } from 'react';
import { PomodoroContext } from '../context/PomodoroContext';

const PomodoroGrid = props => {
    const { tasks } = useContext(PomodoroContext);
    // [...]

    return (
        <div style={{ height: '50%', width: '100%' }}>
            <AgGridReact
                rowData={tasks}
                // [...]
            >
            </AgGridReact>
        </div>
    );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы увидеть это в действии, посмотрите следующий GIF. Обратите внимание, что мы можем переключать таймер как из MainTask, так и из PomodoroGrid в дополнение к тому, что MainTask показывает детали активной задачи.

Обратите внимание, как MainTask показывает активную задачу

Создание пользовательского крючка с использованием useState и useEffect

Приложение pomodoro отображает таймер в MainTask и в колонке Progress каждой строки внутри PomodoroGrid.

В GIF ниже показано, как работает таймер – обратите внимание, как синхронизируются таймеры на MainTask и в столбце Progress при запуске задачи:


useTimer hook ticking

Логика, обрабатывающая тиканье таймера, может быть извлечена в пользовательский хук, поскольку она повторно используется для обоих компонентов. Имя этого пользовательского хука в исходном коде – useTimer.

Хук useTimer принимает три параметра:

Пользовательский хук useTimer определяется следующим образом:

const useTimer = (timerStarted, initialSeconds, taskCompletedCallback) => {
    // [...]
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы определили хук useState seconds для хранения времени, оставшегося на таймере. Он инициализируется с помощью initialSeconds, как показано ниже:

const useTimer = (timerStarted, initialSeconds, taskCompletedCallback) => {
    const [seconds, setSeconds] = useState(initialSeconds);

    // [...]

    return [seconds, setSeconds];
};
Вход в полноэкранный режим Выход из полноэкранного режима

Кортеж seconds и setSeconds возвращается useTimer, чтобы компоненты, использующие useTimer, могли получить seconds.

Для обработки тиканья таймера мы создали хук useEffect, где seconds уменьшается каждую секунду, пока таймер не остановится или seconds не достигнет нуля, в этом случае вызывается taskCompletedCallback:

// src/utils/useTimer.js
import { useEffect, useState } from "react";

const useTimer = (timerStarted, initialSeconds, taskCompletedCallback) => {
    const [seconds, setSeconds] = useState(initialSeconds);

    useEffect(() => {
        let timer;

        if (timerStarted) {
            if (seconds === 0) {
                taskCompletedCallback()
            } else if (seconds > 0) {
                timer = setInterval(() => {
                    setSeconds(seconds - 1)
                }, 1000);
            }
        }

        return () => {
            if (timer) { clearInterval(timer); };
        }

    }, [timerStarted, seconds, taskCompletedCallback]);

    return [seconds, setSeconds];
};

export default useTimer;
Войти в полноэкранный режим Выход из полноэкранного режима

Компонент пользовательского рендеринга ячеек сетки ProgressCellRenderer использует хук useTimer, как показано ниже:

const ProgressCellRenderer = memo(props => {
  const { dispatch, activeTaskId } = useContext(PomodoroContext);
  const { id, timerStarted, timeLeft } = props.node.data;

  const taskCompletedCallback = useCallback(() => {
    dispatch({ type: 'completed_task', id })
  }, [id, dispatch]);

  const [seconds] = useTimer(timerStarted, timeLeft, taskCompletedCallback);

  let timeString = formatSecondsIntoMinutesAndSeconds(seconds);

  return (<>
    <div>
      {timeString}
    </div>
  </>)

});
Вход в полноэкранный режим Выход из полноэкранного режима

В этом случае taskCompletedCallback отправляет действие completed_task, когда оно вызывается, что и приводит к тому, что строка имеет зеленый фон в GIF, показанном выше.

Доступ к Grid API с помощью useRef

Хук useRef позволяет нам получить ссылку на api и columnApi AG Grid, передав ее свойству ref в AgGridReact.

В нашем приложении SaveButton отображает кнопку, которая при нажатии сохраняет текущее состояние в локальное хранилище. Мы используем Grid API для вызова api.showLoadingOverlay(), чтобы уведомить пользователя, что он не может выполнить действие, если активна задача.

Посмотрите на это в действии в следующем GIF, обратите внимание, как таймер работает, пока кнопка нажата, что приводит к появлению накладки:


Вызов Grid API из SaveButton и сохранение состояния в локальном хранилище

Поскольку SaveButton и PomodoroGrid являются родственными компонентами, мы должны определить переменную useRef на родительском App и передать ее обоим компонентам.

// src/App.js
const App = () => {
  const gridRef = useRef(null);

  // [...]

  return (
    <>
      <PomodoroProvider>
        <MainTaskComponent />
        <Grid gridRef={gridRef} />
        <SaveButton gridRef={gridRef} />
      </PomodoroProvider>
    </>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

PomodoroGrid получает useRef hook gridRef как props, который затем инициализируется путем передачи в ref AG Grid:

// src/components/PomodoroGrid.js

const PomodoroGrid = props => {
    // [...]

    return (
        <div style={{ height: '50%', width: '100%' }}>
            <AgGridReact
                ref={props.gridRef}
                // [...]
            >
            </AgGridReact>
        </div>
    );
}
Вход в полноэкранный режим Выход из полноэкранного режима

После того, как PomodoroGrid инициализирует gridRef с API Grid, мы можем получить доступ к методам API из SaveButton, чтобы сохранить список задач в локальном хранилище:

// src/components/SaveButton.js

const SaveButton = props => {
    const { tasks, activeTaskId } = useContext(PomodoroContext);
    const { gridRef } = props;

    const saveHandler = () => {
        if (activeTaskId) {
            let activeTask = tasks.filter(row => row.id === activeTaskId);
            if (activeTask.length > 0) {
                if (activeTask[0].timerStarted) {
                    gridRef.current.api.showLoadingOverlay();
                    setTimeout(() => {
                        gridRef.current.api.hideOverlay();
                    }, 3000);
                    return;
                }
            }
        }
        localStorage.setItem('gridState', JSON.stringify({ tasks, activeTaskId }));
        alert('Saved Grid State to Local Storage');
    }

    return (<div>
            <Button
                // [...]
                onClick={saveHandler}
            >
                Save to Local Storage
            </Button>
        </div>
    )
})
Вход в полноэкранный режим Выход из полноэкранного режима

Резюме

Мы надеемся, что эта статья окажется полезной при использовании AG Grid с React Hooks. Не стесняйтесь форкать пример из этого git-репозитория и модифицировать его в соответствии с вашими потребностями.

Если вы хотите попробовать AG Grid, ознакомьтесь с нашими руководствами по началу работы (JS / React / Angular / Vue).

Оцените статью
Procodings.ru
Добавить комментарий