Как стать автором
Обновить

Комментарии 22

А можно просто воспользоваться MobX:


interface IProps {
  store: SomeStore;
}

class SomeStore {
  constructor() {
    makeObservable(this, {
      numValue: observable,
      strValue: observable,
      increaseNumValue: action,
    });
  }

  numValue = 0;
  strValue = "";

  increaseNumValue(diff: number) {
    this.numValue += diff;
  }
}

const SomeComponent = observer((props: IProps) => {
  const { store } = props;

  useEffect(() => {
    const intervalHandle = setInterval(() => store.increaseNumValue(1), 1000);
    return () => clearInterval(intervalHandle);
  }, [store]);

  return <div>
    <span>{store.numValue}</span>
    <Input type="text" value={store.strValue} 
           onChange={(ev) => store.strValue = ev.target.value} />
    <Button onClick={() => store.numValue = -10}>-10</Button>
  </div>;
});

// instead of  numChanged, stringChanged
reaction(() => store.numValue, (newVal, oldVal) => {
 console.log(`numValue has been changed from ${oldVal} to ${newVal}`);
});

reaction(() => store.strValue, (newVal, oldVal) => {
  console.log(`strValue has been changed from ${oldVal} to ${newVal}`);
});

Я думал, что это шутка — комменты про mobx, в тему и не в тему, в каждом посте про react.


P.S.: arrow functions в хендлерах нужно всё-таки оборачивать. И перечитайте статью.

Я думал, что это шутка — комменты про mobx, в тему и не в тему, в каждом посте про react.

Почему в данном случае это не в тему? Он решает именно ту проблему, которую поставил автор. Лучше или хуже — это уже другой вопрос.


P.S.: arrow functions в хендлерах нужно всё-таки оборачивать. И перечитайте статью.

В данном случае не обязательно их использовать. Перечитайте документацию про useCallback и когда его следует использовать.

store на каждый рендер — новый, Вы к этому? И Button будет перерендериваться? Зачем?
Не, спорить не буду, каждый д… каждому своё.


Лучше или хуже — это уже другой вопрос

Т.е. Вы намеренно предложили решение, которое может быть хуже? Зачем? Это контринтуитивно.

Во-во

Ну а ещё проще использовать $mol:


$my_app $mol_page
    num?val 0
    str?val \
    body /
        <= Num $mol_view sub / <= num
        <= Str $mol_string value?val <=> str?val \
        <= Dec $mol_button_minor
            title \-1
            click?event <=> dec?event null

class $my_app extends $.$my_app {

    @ $mol_mem
    auto() {
        this.num( this.num() + 1 )
        return new $mol_after_timeout( 1000, $mol_atom2.current.fresh )
    }

    dec() {
        this.num( this.num() - 10 )
    }

}

Но разве не понятно, что автору не нужно как проще? Автору нужно сделать буханку из троллейбуса!

Зачем сразу столько негатива? Мой пример на MobX кого-то оскорбил?


Ну а можно просто использовать $mol:

Лично мне интересно увидеть, как это в $mol реализовано. Даже появилось желание узнать про него больше.

Это ирония. Обычно это под моим комментарием толпятся, мол "как же ты достал со своим молом под каждым постом о любом фреймворке, не мешай нам вырывать гланды через анус".

Ну почему, я с интересом жду Ваши комментарии. По крайней мере — не скучно.

Так какую проблему вы решали? Если родителю нужно знать состояние вашего компонента, ну так вытесните это состояние полностью в родителя. Часто в реальности родителю нужно не только "сигнализировать" о изменении стояния, но и он должен иметь возможность его инициализировать или даже менять. Т.е. Ваш первый вариант станет чем-то таким


export type ReactStateAndSetter<S> = [S, React.Dispatch<React.SetStateAction<S>>];

export interface IProps {
    numValueState: ReactStateAndSetter<number>;
    strValueState: ReactStateAndSetter<string>;
}

export function SomeComponent({numValueState, strValueState}: IProps) {
    const [numValue, setNumValue] = numValueState;
    const [strValue, setStrValue] = strValueState;
    ...
}

Плюс в приложении не будет двух копий одного состояния — в текущем компоненте и в родителе.
Если же прямо хочется передавать только колбэк в пропы, то useReducer, например, может помочь привязать колбэк к изменению состояния более собрано (в одном месте кода, а не состояние + обертка).
К useCallback() это ситуация вообще никак не относится. Вы вот пишите:


элементы input и button заменим компонентами Input и Button, которые потребуют обернуть обработчики событий хуком useCallback

А почему до замены не нужно было оборачивать? Я сейчас не к тому, зачем вообще useCallback, я к тому, что это не связанные вещи.

А почему до замены не нужно было оборачивать?

Потому что до этого они не использовались в useEffect или других useCallback.

Сразу извинюсь за стиль изложения — писатель я неопытный. Пример полностью синтетический и бессмысленный. Обёртками useCallback я намеренно замусорил код (а чтоб обосновать их применение перешел от простых элементов input и button к вымышленным компонентам Input и Button), чтоб показать эффективность собственного приёма (useMembers), при использовании которого useCallback не понадобятся.
Прием с использованием useMembers пришел мне в голову на гораздо более сложном примере, когда функциональный компонент раздулся до неприличных размеров, массивы зависимостей у функций вырастали элементов до 7. Я уже отчаялся отладить этот компонент, переписал его в виде класса, отладил, а позже придумал вынести обработчики событий из функционального компонента в методы класса.
К моему удивлению, такого метода нигде в интернете не нашел, поэтому решил поделиться здесь.
А почему до замены не нужно было оборачивать? Я сейчас не к тому, зачем вообще useCallback, я к тому, что это не связанные вещи.

В React компоненты Input и Button следует передавать колбэки, обёрнутые в useCallback, для предотвращения ненужных рендеров (при условии, что эти компонеты обёрнуты в React.memo() либо реализуют shouldComponentUpdate). Для input И button оборачивание в useCallback никакой пользы не принесёт.
Не так давно был на хабре пример с идеей использовать классы с помощью useRef:
habr.com/ru/post/541884/#comment_22689676
Я эту идею тоже потом использовал в своей недавней статье.

Ваш пример можно упростить.
Вынесите хуки тоже в класс, тогда не придеться передавать зависимости от хуков этого компонента и не нужно будет возвращать функции-эффекты.
По аналогии с setDeps создайте метод, который постоянно вызывается в useMembers и вызывайте в нем ваши хуки. Т.к. этот метод будет вызываться при каждом вызове функционального компонента, то для react он не будет отличаться от custom hook.
class Members extends MembersBase<IDeps> {
    // …
    onRender = () => {
        useEffect(this.intervalEffect, []);
        const [numValue, setNumValue] = useState(0);
        this.numValueStatePair = {numValue, setNumValue};
    };
}

export function useMembers<D, T extends MembersBase<D>>(ctor: (new () => T), deps:  (T extends MembersBase<infer D> ? D : never)): T {
    // ...
    rv.setDeps(deps);
    rv.onRender(); // вызов метода
    return rv;
}


Да и для компонентов-классов можно написать обертки над методами жизненных циклов и setState, чтобы их задавать в компоненте-классе по аналогии с useEffect, useState. Правда useState не будет так же красиво выглядеть.
Ваш пример можно упростить.
Вынесите хуки тоже в класс, тогда не придется передавать зависимости от хуков этого компонента и не нужно будет возвращать функции-эффекты.

Если я всё правильно понял, вы предлагаете писать так:
Код
interface IMembers<T> {
    onRender(props: T): JSX.Element;
}

function useMembers<P, T extends IMembers<P>>(ctor: (new () => T), props: P) {
    const ref = useRef<T>();
    if (!ref.current) {
        ref.current = new ctor();
    }
    ref.current.onRender(props);
}

export function SomeComponent(props: IProps) {
    return useMembers(Members, props);
}

class Members extends IMembers<IProps> {
    ...
    private deps: IDeps;

    onRender = (props: IProps) => {
        const [numValue, setNumValue] = React.useState(0);
        const [strValue, setStrValue] = React.useState("");
        
        this.deps = { props, numValue, setNumValue, setStrValue };
        
        React.useEffect(this.intervalEffect, []);

        return <div>
            <span>{`Число = ${numValue}`}</span>
            <Input type="text" onChange={this.onTextChanged} value={strValue} />
            <Button onClick={this.onBtnClick}>-10</Button>
        </div>;
    }
    ...
}

Функция компонента опустела, код полностью переехал в класс. Что мы выиграли? Отказались от одной операции деструктуризации объекта
const { onTextChanged, onBtnClick, intervalEffect } = useMembers(...
, добавив префиксы this к названиям функций. Перенесли объявление
private deps: IDeps;
в реализацию класса.
На мой взгляд — сомнительное упрощение. Читаемость кода ухудшилась. Я предпочту остаться в парадигме функционального компонента: хуки в основной функции, перенося в класс только громоздкие функции с зависимостями.
А за комментарий спасибо.
Почти правильно поняли. Только JSX код остается в компоненте, а не переносится в onRender (неудачно назвал).
Согласен с минусами переноса хуков в класс. Показалось, что если в данном случае отделить логику от представления, то получится лучше.

Собственно, я предложил перенести хуки в класс, т.к. в коде из статье мне не очень понравилось разделение эффекта на вызов через useEffect и его объявление в классе. С одной стороны это гибче, а с другой стороны нет информации о том, как его использовать в компоненте. Для этого нужно посмотреть его реализацию. К тому же реализация может измениться, но кто-нибудь забудет изменить код его использования в useEffect. Т.е. возможны 3 варианта использования:
useEffect(intervalEffect);
useEffect(intervalEffect, []);
useEffect(intervalEffect, [x, y]);
но 2 из них скорее всего будут ошибочными.
Предлагаю выносить загромождающие код обработчики в объект класса вместе с зависимостями. Разве так не лучше?

Довольно странно видеть, как автор попробовав путь функциональных хуков вернулся, пусть и не явно, к использованию привычного набора методов объеденных в класс, получив все те недостатки, что были в классовых React компонентах и проигнорировав все те преимущества, что несут функциональные React хуки.

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

1. Хук позволяющий вызвать коллбэк с заданным интервалом:

function useInterval<T extends () => void>(cb: T, timeout = 1000) {
  useEffect(() => {
    const intervalHandle = setInterval(cb, timeout);
    return () => clearInterval(intervalHandle);
  }, [cb, timeout]);
}

2. Хук позволяющий хранить некое значение на изменение которого можно подписаться:

function useValue<V>(
  initValue: V,
  onValueChange?: (nextValue: V) => void
): [V, (nextValue: V | ((prevValue: V) => V)) => void] {
  const [value, setValue] = useState(initValue);

  useEffect(() => {
    if (onValueChange) {
      onValueChange(value);
    }
  }, [value, onValueChange]);

  return [value, setValue];
}

3. Хук создающий обработчик для input элементов:

function useInputHandler(handler: (value: string) => void) {
  return useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      handler(e.target.value);
    },
    [handler]
  );
}

4. Хук создающий обработчик изменяющий значение на заданную величину:

function useNumHandler(
  diff: number,
  setNumValue: (value: (prevValue: number) => number) => void
) {
  return useCallback(() => {
    setNumValue((value) => value + diff);
  }, [diff, setNumValue]);
}

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

interface IProps {
  numChanged?: (sum: number) => void;
  stringChanged?: (concatRezult: string) => void;
}

export function SomeComponent(props: IProps) {
  const { numChanged, stringChanged } = props;
  const [numValue, setNumValue] = useValue<number>(0, numChanged);
  const [strValue, setStrValue] = useValue<string>("", stringChanged);
  const onTextChanged = useInputHandler(setStrValue);
  const incNumByOne = useNumHandler(1, setNumValue);
  const decNumByTen = useNumHandler(-10, setNumValue);

  useInterval(incNumByOne);

  return (
    <div>
      <span>{numValue}</span>
      <input type="text" onChange={onTextChanged} value={strValue} />
      <button type="button" onClick={decNumByTen}>
        -10
      </button>
    </div>
  );
}


Ну и в заключении, никто не говорит, что код на хуках лучше или хуже классов, выбирайте то что вам по душе и используйте с умом.
Немного неэквивалентно: хук useValue вызывает onValueChange лишь после рендера, хотя на самом деле может быть так и лучше. Сложнее. Но чертовски элегантно. Аплодирую стоя. Спасибо.
  const { numChanged, stringChanged } = props;
  const [numValue, setNumValue] = useValue<number>(0, numChanged);
  const [strValue, setStrValue] = useValue<string>("", stringChanged);
  const onTextChanged = useInputHandler(setStrValue);
  const incNumByOne = useNumHandler(1, setNumValue);
  const decNumByTen = useNumHandler(-10, setNumValue);

  useInterval(incNumByOne);

  let num = 0
  let str = ""
  const onTextChanged = v => str = v
  const incNumByOne = ()=> num += 1
  const decNumByTen = ()=> num -= 10

  setInterval(incNumByOne, 100);

Где-то мы пропустили поворот..

А ещё useValue сигнализирует об изменении value при изменении ссылки на onValueChange. И редко кто задумывается об этом. Приходится костылить с всякими useBoundCallback, чтобы иметь стабильную ссылку на callback который будет в актуальном замыкании.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации