Edited at

【React】新機能hooks


はじめに

会社で色々な事件に巻き込まれしまってなかなか書けなかった不幸な日々でした。

ちょっと乗り遅れた感あるんですが、自分の勉強と理解がてら新しい機能であるhooksを書いていきます。


hooks

hooksはclassを書くことなくstateとか、色々な機能を使うことができる新しい機能です!React v16.7.0-alphaから使えます。

create-react-appでつくると、僕がやってみたときは16.6だったので、ちょっとアップデートしないとできないので注意です。

そして、かつてはSFCと呼ばれる関数定義のコンポーネントたちも、ここではSFCではなくFCになります。

なにせstatelessではないからね・・・!!。


state

この機能を使うと、functional componentがstateを扱えるようになります。ちゃんとre-renderされようとも値を保持してくれるみたいです。

useStateの書式はこうなります。

const a = useState(b);

a: [currentState, dispatchAction]
b: initialState

こんな感じでuseStateは現在値と、setStateに似た関数を返します。

import React, { useState } from 'react';

export default () => {
const [a, b] = useState(0);
return (
<div>
<button onClick={() => { b(a + 1); }}>
\ovo/ {"<"}{a} times!!
</button>
</div>
)
};

ざっとこんな感じになります。


this.stateと異なり、objectを使うべきではない使う必要はない。・・・・しかし、もし望むならそれも可能だ。initialStateは初回だけ使われるであろう・・・・。


的なことが書いてあります。

ということでやってみましょう。

import React, { useState } from 'react';

export default () => {
const [a, b] = useState({
count: 0,
str: 'hello',
});
const {
count,
str,
} = a;
console.log(a);
return (
<div>
<button onClick={() => {b({ count: count + 1 })}}>
\ovo/ {"<"} {count}times!! {str}!!
</button>
</div>
)
};

スクリーンショット 2018-11-02 15.51.34.png

見事に消滅しましたね。

結果として、第一引数と第一引数を変更する値を保持している関数と言った感じですね。

setStateはマージしてくれるのですが、useStateを使った場合は完全に上書きされるので注意が必要です!

オブジェクトで管理しようとすると自力で保持することが必要になり、自力保持できなくなった瞬間あぼーんするので、複数のstateを使いたい場合は、次の複数宣言をするといいかもしれません。


Declaring multiple state variables

array destructingを使うことによっていろんな名前を持たせることもできます。

ゆえに、複数のstateを管理したいのであれば、複数回useStateを使えばいいと言うことになります。

import React, { useState } from 'react';

export default () => {
const [a, b] = useState(0);
const [_a, _b] = useState('!');

return (
<div>
<button onClick={() => { b(a + 1); }}>
\ovo/ {"<"} {a}times!!
</button>
<button onClick={() => { _b(`${_a}!`)}}>
\omo/ {"<"} hello{_a}
</button>
</div>
)
};

と言った感じで従来の書き方だと、こうだったところですね。

state = {

a: 0,
_a: '!',
};


effect

データ取得やDOM変更といったside effectsを扱うためにはこのhookを使います。

effect hookは以前からあるcomponentDidMountcomponentDidUpdatecomponentWillUnmountと同じ目的で使うことができるみたいです。

import React, { useState, useEffect } from 'react';

export default () => {
const [a, b] = useState(0);
const [_a, _b] = useState('!');

useEffect(() => {
alert(`updated! ${a}times!!`);
});

return (
<div>
<button onClick={() => { b(a + 1); }}>
\ovo/ {"<"} {a}times!!
</button>
<button onClick={() => { _b(`${_a}!`)}}>
\omo/ {"<"} hello{_a}
</button>
</div>
)
};

こうしてみるとわかるんですが、まずcomponentDidmountと同じようなタイミングでuseEffectにある関数が発火しています。

なので、

初回に updated! 0times!とアラートが表示されます。

次に、ボタンを押すと、これまたalertが再び発火するといったことができます。

デフォルトだと、renderのたびにeffectが呼ばれることになります。

そして、すでにこのサンプルでお気づきかもしれませんが、現状のままだと、helloの方を押してもuseEffectが発火します・・・。

そこで、第二引数の登場です。


skipping effect

第二引数に、アレイの形で比較対象を渡すことで、判定をおこないます。

  useEffect(() => {

alert(`updated! ${count}times!!`);
}, [count]);

つまり、こうするんです。

すると、countが変化したかを判定してくれて、prev === currentであればeffectをスキップするということが可能になっています。

もう少し深掘りしていくと、

  useEffect(() => {

alert(`updated! ${count}times!!`);
}, [Math.floor(count/3)]);

こちらのケースでは3回に1回発火します。

count: 1 -> 0

count: 2 -> 0
count: 3 -> 1 (0 -> 1になったので発火)
count: 4 -> 1
count: 5 -> 1
count: 6 -> 2 (1 -> 2になったので発火)

小ネタですが、一度だけ動かしたいのであれば[]を使うことで一度だけ発火します。

  useEffect(() => {

console.log('------', count);
}, []);

componentDidMountとかはこんな感じですね。

複数の値の検知も行ってくれるので

import React, { useState, useEffect } from 'react';

const Sub = (props) => {
const {
count,
hello,
} = props;

useEffect(() => {
console.log('------', count);
}, [hello, count]);

return (
<div>
{count}
</div>
)
};

export default () => {
const [a, b] = useState(0);
const [_a, _b] = useState('!');

return (
<div>
<Sub count={a} hello={_a} />
<button onClick={() => { b(a + 1); }}>
\ovo/ {"<"} {a}times!!
</button>
<button onClick={() => { _b(`${_a}!`)}}>
\omo/ {"<"} hello{_a}
</button>
</div>
)
};

こうすると、countかhelloのどちらかが変更されれば発火するというギミックを組めます。


cleanup

関数を返すことでクリーンアップの処理を挟むことができます。

  useEffect(() => {

console.log('subscribe', count);
console.log('---------------');

return () => {
console.log('unsubscribe', count)
};
});

例えばこちらの例。でいくと、ログはこうなります。

subscribe 0

--------------- ボタン押した。
unsubscribe 0
subscribe 1
---------------  ボタン押した。
unsubscribe 1
subscribe 2
---------------  ボタン押した。
unsubscribe 2
subscribe 3

最後に関数を返すと次回、effectが処理を行うときに前もってcleanupを実行してくれるようになるという挙動です。


Building Your Own Hooks

一応紹介だけしておくと、他のとこでも呼べるよと言う話。

import React, { useState, useEffect } from 'react';

const useStatus = () => {
const [id, setId] = useState(0);
const newId = id + 1;

return {
newId,
dispatch: setId,
};
};

export default () => {
const {
newId,
dispatch,
} = useStatus();

return (
<div>
<button onClick={() => { dispatch(newId)}}>
\owo/ {"<"} status{newId}
</button>
</div>
)
};

useという プレフィックスをつけるとlinterがどうのこうのと書いてありますが基本それだけで動作上はuseとつけてuseStatusにする必要はないです。

eslint-plugin-react-hooksこれ使おうなって話なだけです。

ここら辺の話は、Rules of Hooksで書かれてます。


useContext

contextを使うことができるやつです。

contextに思い入れがないため、感想薄いです。

-- 追記 --

@mrsekut さんからアドバイスいただいてコード更新しました!

import React, { useContext, createContext, useState } from 'react';

const context = createContext();
const { Provider } = context;

const Child = () => {
const count = useContext(context);
return (
<div>
<span>{count.count}</span>
<button onClick={count.increment}>+</button>
<button onClick={count.decrement}>-</button>
</div>
);
};

export default () => {
const [count, setCount] = useState(0);
return (
<Provider
value={{
count,
increment: () => setCount(count + 1),
decrement: () => setCount(count - 1)
}}
>
<Child />
</Provider>
);
};


Hooks APIs

これが僕がとても楽しみにしていた機能群!


useReducer

これがとても楽しみでたまらなかった人はたくさんいるのでは・・・????ってぐらい、楽しみだった機能。

仕事が忙しくて触れなくて悲しかった。もっと自由に時間が割ける環境にいこうかなぁ・・・・・。

めっちゃ公式通りで恐縮なんですが、

こんな感じです。

import React, { useState, useReducer } from 'react';

const initialState = {
count: 0,
};

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

const reducer = (state, action) => {
const {
type,
} = action;
const {
count,
} = state;

switch (type) {
case INCREMENT:
return {
...state,
count: count + 1,
};

case DECREMENT:
return {
...state,
count: count - 1,
};

case RESET:
return initialState;

default:
return state;
}
};

export default () => {
const [
state,
dispatch,
] = useReducer(reducer, initialState);
const {
count,
} = state;

return (
<div>
<button onClick={() => { dispatch({ type: INCREMENT }); }}>
add
</button>
<button onClick={() => { dispatch({ type: DECREMENT }); }}>
del
</button>
<button onClick={() => { dispatch({ type: RESET }); }}>
reset
</button>
\owo/ {"<"} {count} happy!!

</div>
)
};

useStateにreducerを突っ込んでいる形になりますね。

なので当然こんなこともできちゃいます。

  const [

state,
dispatch,
] = useReducer(reducer, initialState);
const [
state2,
dispatch2,
] = useReducer(reducer, initialState);

これやる意味があるのかと言われると複雑ですが・・・・。

引数は3つとれて、返り値は2つです。

[state, dispatch] = useReducer(reducer, initialState, initialAction);

initialActionに書いたものは、初回render後に発火する手筈みたいです。

なので、初期化するときに任意の値を入れ込むと言ったことがここでできますね。

ちょっとreduxと違うところとしては結局useStateに近い概念なところですね。なので、connectみたいな感じでどっからでもstore取ってこれるかというとそうでも無いところがちょっと気になりますね。

これででかい奴を作ったことがないので、ちょっとこれから検討になりますが、ベストプラクティスはなんだろうかと思います。はしご渡しだけは絶対にないとは思ってます。


useMemo

コールバックをしてくれます。

なんか、こころやさしいおばはんみたいな機能ですかね。

import React, { useState, useMemo } from 'react';

export default () => {
const [count, setCount] = useState(0);
const newValue = useMemo(() => {
console.log('Don\'t you forget?', count);

return `${count} push`;
}, [count]);

console.log(newValue);

return (
<div>
<button onClick={() => { setCount(count + 1); }}>
\owo/ {"<"} {count} times!
</button>
</div>
)
};

値が変更されるたびに内部の関数が実行されます。

なので、ボタンを押すとこんな感じで値が出力されます。

Don't you forget? 0

0 push
Don't you forget? 1
1 push
Don't you forget? 2
2 push

インターフェースは、

returnValue = useMemo(function, watchValues);

となっています。

これの面白いところとしては値が保持されることです。

watchValuesに何も与えないと、functionが実行されないので試してみます。

const newValue = useMemo(() => {

console.log('Don\'t you forget?', count);

return `${count} push`;
}, []);

console.log(newValue);

Don't you forget? 0

0 push
0 push
0 push

となります。

初回は実行されて、以後実行されなくなるので、直前に保持された0 pushがuseMemoの返り値となります。

じゃあ今度は、こうしてみましょう。

const newValue = useMemo(() => {

console.log('Don\'t you forget?', count);

return `${count} push`;
}, [Math.floor(count/3)]);

console.log(newValue);

Don't you forget? 0

0 push
0 push -> このときcount:1
0 push -> count:2
Don't you forget? 3 -> count:3 でwatchValuesが更新されるので関数が発火。
3push -> count: 3を認識して返り値変更

となります。


useCallback

useMemoと基本的には一緒です。

ただし一つだけ違う点があります。

import React, { useState, useCallback } from 'react';

export default () => {
const [count, setCount] = useState(0);
const newValue = useCallback(() => {
console.log('Don\'t you forget?', count);

return `${count} push`;
}, [Math.floor(count/3)]);

console.log(newValue());

return (
<div>
<button onClick={() => { setCount(count + 1); }}>
\owo/ {"<"} {count} times!
</button>
</div>
)
};

返り値が関数になっている点です。

なので

console.log(newValue());

こうしてます。


useRef

useRefを使うことでref objectが返ります。それをref属性として渡すことによって、refを扱うことができます。

import React, { useState, useRef } from 'react';

export default () => {
const [count, setCount] = useState(0);
const buttonElement = useRef(null);

console.log(buttonElement);

return (
<div>
<button ref={buttonElement} onClick={() => { setCount(count + 1); }}>
\owo/ {"<"} {count} times!
</button>
</div>
)
};


useImperativeMethods

refに対して処理を紐づけることができます。

import React, { useRef, useImperativeMethods } from 'react';

export default () => {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
useImperativeMethods(inputEl, () => ({
focus: () => {
console.log('focus');
},
}));

return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
};

このコードでは、inputElにfocusが割り当てられています。

ボタンを押すことによって、focusが実行されるので、useImperativeMethodsのfocusも実行されるという手筈になっています。

いつものように第三引数にwatchValuesを入れることができます。

import React, { useRef, useImperativeMethods, useState } from 'react';

export default () => {
const [count, setCount] = useState(0);
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
useImperativeMethods(inputEl, () => ({
focus: () => {
console.log('focus', count);
},
}), [Math.floor(count/3)]);

return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
<button onClick={() => { setCount(count + 1); }}>
{count}
</button>
</>
);
};

こうすると、countが 0 ~ 2の間は、focusボタンを押したときに

focus 0

と出力されますが、countが3のときは 

focus 3

が出力されます。


おまけ

forwardRefがどーたらとドキュメントに書かれたのでforwardRefを使ったサンプルを書きました。

ちょっとややこしいサンプルで申し訳ないです・・・。

こうすると、testボタンを押すと、hiボタンを押したことになります。

import React, { useRef, useImperativeMethods, forwardRef } from 'react';

const createButton = (props, ref) => {
const buttonElement = useRef(null);
useImperativeMethods(ref, () => ({
click: () => {
buttonElement.current.click();
},
}));

return <button ref={buttonElement} onClick={() => { console.log('click child');}}> hi </button>;
};
const CustomButton = forwardRef(createButton);

export default () => {
const newButtonElement = useRef(null);
const parentButtonClick = () => {
newButtonElement.current.click();
};

return (
<div>
<CustomButton ref={newButtonElement} />
<button onClick={parentButtonClick}>
test
</button>
</div>
)
};

と言うのも、

export default () => {

const newButtonElement = useRef(null);
const parentButtonClick = () => {
newButtonElement.current.click();
};

return (
<div>
<CustomButton ref={newButtonElement} />
<button onClick={parentButtonClick}>
test
</button>
</div>
)
};

testボタンは押すとnewButtonElement.current.click()を実行するように書かれています。

次に、

 <CustomButton ref={newButtonElement} />

これによって、newButtonElement=CustomButtonとなります。

const createButton = (props, ref) => {

const buttonElement = useRef(null);
useImperativeMethods(ref, () => ({
click: () => {
buttonElement.current.click();
},
}));

return <button ref={buttonElement} onClick={() => { console.log('click child');}}> hi </button>;
};
c

refがわたされたcreateButtonは、渡されたrefをuseImperativeMethosに渡しているため、このような挙動になります。


おわりに

以上です。

ちょっと感動したものは厚めに、そうで無いものは薄めにといった紹介になってしまいすみません。

このサンプル書くまでは、useStateの何が嬉しいのかと思ってたんですが、これ書きながら何回もuseStateって書いてたら、こっちの方が楽なきがしてきました。

useState, useEffect, useMemoは使い所がたくさんあると思うのでぜひこれから使っていきたいと思います。

反面useReducerすごいけど、これどうやってつかおう・・・。って感じが結構あります。useContextと組み合わせて使うのが正かなぁという気がしないでも無いですが自信はないですね・・・。

とりわけrecomposeとかもあまり好きではなかったのでこんな簡単に色々とできるようになってさぞかしSFC、いやFCも幸せだろうと思います!僕もFC多用していきたいと思いますー!