Хабрахабр

getDerivedStateFromState – или как сделать из простой проблемы сложную

Я люблю Реакт. Люблю за то, как он работает. За то, что он делает вещи «правильно». HOC, Composition, RenderProps, Stateless, Stateful – миллион патернов и антипатернов которые помогают меньше косячить.
И вот совсем недавно React принес нам очередной подарок. Очередную возможность косячить меньше — getDeviredStateFromProps.
Технически — имея статический мапинг из пропсов в стейт логика приложения должна стать более проста, более понятна, тестируема и так далее. По факту многие люди начали топать ногами, и требовать prevProps обратно, не в силах (или без особого желания) переделать логику своего приложения.
В общем разверлись пучины ада. Ранее простая задача стала сложней.

Изначальная дискуссия развернулась на страницах github/reactjs.org, и была вызвана необходимостью знать как именно поменялись props, в целях логирования

We have found a scenario where the removal of componentWillReceiveProps will encourage us to write worse code than we currently do.

// OLD WAY
componentWillReceiveProps(newProps)
}
// NEW WAY
static getDerivedStateFromProps(nextProps, prevState){ if (this.state.visible === true && nextProps.visible === false) { registerLog('dialog is hidden'); } return { visible : nextProps.visible };
}

PS: Но вы то знаете, что такие операции надо выполнять в `componentDidUpdate`?

В тот же день был (пере)создан issue о модификации getDerivedStateFromProps, потому что без prevProps жизни нет никакой. Но это было только начало. Так ему и надо. Точно такой же issue уже был единожды закрыт с «Wont fix», и на этот раз, после долгих словестных баталей, он опять же был закрыт с «Wont fix».

Но, перед тем как обсудить выход из положения, и почему issue был закрыт — лучше придумать какой-либо удобный пример для наглядности рассуждений.

Таблица. С сортировкой и постраничной навигацией.

Обратимся к TDD, и в начале определим задачу, и пути ее решения

  1. Что нужно сделать чтобы нарисовать таблицу?
    1. Взять данные для отображения
    2. Отсортировать их
    3. Взять slice, с данными только для текущей страницы
    4. Не перепутать порядок пунктов

  2. Что делать если данные изменились?
    1. Начать все с начала

  3. А если изменилась только страница?
    1. Выполнить пункт 1.3 и далее.

  4. Как изменить страницу
    1. this.setState({page})

  5. Как отреагировать на изменение state.page?
    1. Никак

В том и проблема — можно отреагировать на изменение props, но для изменение стейта такой функции нет (даже если вы прочитали ее в названии этой статьи).

Правильное решение номер 1

Точнее «правильное» решение. Я думаю это должен быть конечный автомат. Изначально он находится в состоянии idle. При поступлении сигнала setState({page}) он перейдет в другое состояние — changing page. При входе в это состояние он посчитает что там ему надо и пошлет сигнал setState({temporalResult}). По хорошему далее автомат должен перейти в состояние «next step», который посчитает все что угодно из шага после текущего, и в итоге попадает в commit, и где передаст данные из temporalResult в data, после чего перейти в idle.
Технически — это правильное решение, и возможно все так и работает, где-то глубоко в железе, или листочке бумаги. Пускай там и остается.

Правильное решение номер 2

А что если создать еще один элемент, в который передать в виде пропсов state и props из текущего элемента, и использовать getDerivedStateFromProps?
Тоесть «первый» компонент — это «smart» controller, в котором происходит setState({page}), а его dumb будет не такая уж и dump, вычисляя нужные данные при изменении внешних параметров.
Все хорошо, но пункт «пересчитать только то что нужно» не реализуем, так как мы ЗНАЕМ что что-то изменилось (потому что кто-то вызвал getDerivedStateFromProps), но не знаем ЧТО.
В этом плане не изменилось ни-че-го.

Правильное решение номер 3 («официальное»)

Основой «решения», которое и послужило аргументаций закрытия issue, было одно простое утверждение.

You need memoization. You might not need redux getDerivedStateFromProps.

// base - https://github.com/reactjs/rfcs/pull/40#discussion_r180818891
import memoize from "lodash.memoize"; class Example { getSortedData = memoize((list, sortFn) => list.slice().sort(sortFn)) getPagedData = memoize((list, page) => list.slice(page*10, (page+1)*10)) render() { const sorted = this.getSortedData(this.props.data, this.props.sort); const pages = this.getPagedData(sorted, this.props.page); // Render with this.props, this.state, and derived values ... }
}

Мемоизация и будет следить за «изменениями», потому что она просто знает «старые» значения, и вызывает мемоизированную функцию только когда значение изменяется.

И обе я взял из второго комментария к оригинальному issue Но тут есть две проблемы.

Проблема номер 1

I'm having to resort to a weird multi-depth WeakMap, and making decisions about when to drop different levels of the cache.

Тот самый «значимый» порядок изменения значений, помноженный на кривые руки. Появляются какие-то уровни кеширования, WeakMaps. Охо, что ты делаешь, остановись!

Проблема номер 2

One solution suggested memoizing that computation and calling it each time, which is a good idea but in practice it means managing caches which, when you're dealing with a function that takes more than one argument, greatly increases your surface area for potential bugs and mistakes.

А это одна из главных проблем всех библиотек мемоизации — требования использования «конечных» значений как аргументов функции. В общем просто неудобно, а заодно можно переменну перепутать.

В каскадах reselect, когда имея два мемоизированных значения на вход, можно сформировать третье мемоизированное значение на выход.
Еще лучше — композиция мемоизированных функций, когда вы просто определяете порядок исполнения, а некий (конечный) автомат исполняет их одно за другим… Вообще каскады reselect это тоже «composing», но у них там дерево, а тут нужен линейный процесс — waterfall.
Первая проблема имеет решение в reselect.

К чему бы это? Хм, я видел водопад в анонсе этот статьи.

const input = {...this.state, ...this.props }; const resultOfStep1 = {...input, sorted:this.getSortedData(input.data, input.sort); const resultOfStep1 = {... resultOfStep1, sorted:this.getPagedData(resultOfStep1.sorted, resultOfStep1.page);

Если «весь мусор» вынести в хеплер, то получим достаточно чистый код

const Flow = (input, fns) => fns.reduce( (acc,fn) => ({...acc, ...fn(acc)}), input); const result = Flow({...this.state, ...this.props },[ ({ data, sort }) => ({data: this.getSortedData(data, sort) }); ({ data, page }) => ({data: this.getPagedData(data, page) ]);

Чистое, простое и очень красивое решение для проблемы номер 1, четко определяющее порядок формирования конечного значение, которое совершенно не возможно мемоизировать.
Которое совершенно не возможно мемоизировать потому что у «шага» исполнения только один аргумент, и при любом изменении input надо начинать с самого первого этапа — нельзя понять что изменился только page и надо перезапустить только последний шаг.

Или можно?

import {MemoizedFlow} from "react-memoize"; class Example { getSortedData = (list, sortFn) => list.slice().sort(sortFn) getPagedData = (list, page) => list.slice(page*10, (page+1)*10)) render() { return ( <MemoizedFlow input={this.props} flow = [ ({data, sort}) => ({ data: this.getSortedData(data, sort)}), ({data, page}) => ({ data: this.getPagedData(sorted, page)}); ] >{ ({data}) => <table>this is data you are looking for {data}</table> } </MemoizedFlow> ) }
}

И даже функция Flow, которая будет использована для расчета конечного значения будет точно такая же, как и раньше.
Весь секрет — в другой функции мемоизации, memoize-state, про которую я расказывал месяц назад — она то и знает какие части state были использованны на конкретном этапе, давая возможность реальзовать мемоизированный waterfall.
Как не странно — на этот раз все будет работать как часики.

Более сложный пример на поиграться — codesandbox.io/s/23ykx5z5jp

Можно даже сайдэффекты запускать (работает, но лучше не надо).
И самое главное — такой подход позволяет определить именно реацию на изменение параметра. В итоге — статическая функция getDerivedStateFromProps заменяется на (в неком смысле) статически определенный компонент, настройка которого позволяет четко определить «способ и метод» получения результата, точнее формирование конечного результата из набора исходных данных.
Это может быть getDerivedStateFromProps, getDerivedStateFromState, getDerivedPropsFromProps — все что угодно. И позволяет определить именно в том виде который «правильный»

А не только если «страница». Данные надо обновить если изменились даные, или страница.

Однаждый определенный Flow невозможно сломать. Главное перестать хотеть знать старые значения.

Заключение

В общем React последнее время учит нас «не хотеть» различные подходы, которые могут привести к говнокоду, или проблемам с асинхронным рендером. Но люди остаются людьми, и не хотят отказываться от старых, проверенных временем подходов. Именно в этом и проблема.

Все проблемы можно решить, главное просто попытаться взглянуть на проблему под другим углом. На самом деле иногда очень сложно понять как сегодня «правильно» готовить реакт, ведь буквально две недели назад вы его готовили, а тут БАЦ и рецепт изменился.
Но не отчаивайтесь — memoize-state и react-memoize построенный на его основе немного притупят болевые ощущения.

PS: Тот самый оригинальный issue с заключением — github.com/reactjs/rfcs/pull/40#discussion_r180818891
PS: Немного про то как и почему memoize-state работает — habrahabr.ru/post/350562

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть