以下は、jsmanifestさんの記事、8 Useful Practices for React Apps You Should Knowの日本語訳です。
【必読】Reactアプリで役に立つプラクティス8選(8 Useful Practices for React Apps You Should Know)
mediumで私を見つけてくださいね。
Reactはとても多くのステージへの変化を行ってきました。そしてそれらは、必ず私達を驚かせます。
最初、私達はmixinsを使って、インターフェースを作ったり管理したりしていました。そして次にクラスコンポーネントというコンセプトが到来し、現在は、Reactでの私達の開発方法を変えるreact hooksです。
他になにか素晴らしいことを知っていますか? 例えば、Reactで使える、アプリをより良くするのに役立つ巧みなトリックです。
この記事では、全てのReactデベロッパーが知っておくべき8つの高度なトリックを紹介します。このリストの全てがあなたにとって目新しいものではないと思いまが、最低でも1つ、便利なトリックを見つけることを願っています。
これが、あなたが知っておくべきreactで使える8つのトリックです。
1. 文字列でReact要素を生成する(Create react elements with strings)
まず1つ目は、HTML DOMタグを表すシンプルな文字列でReact DOM要素を作る方法です。
例えば、変数にdiv
という文字列を代入することで、Reactコンポーネントを作ることができます。
import React from 'react'
const MyComponent = 'div'
function App() {
return (
<div>
<h1>Hello</h1>
<hr />
<MyComponent>
<h3>I am inside a {'<div />'} element</h3>
</MyComponent>
</div>
)
}
Reactは、React.createElement
を呼び、与えられた文字列を使って、内部で要素を生成します。これってクールでしょ?
Material-UIのようなコンポーネントライブラリでよく使われます。コンポーネントのルートnodeを決めるcomponent
propsを宣言できます。
function MyComponent({ component: Component = 'div', name, age, email }) {
return (
<Component>
<h1>Hi {name}</h1>
<div>
<h6>You are {age} years old</h6>
<small>Your email is {email}</small>
</div>
</Component>
)
}
このように使用することができます。
function App() {
return (
<div>
<MyComponent component="div" name="George" age={16} email="george@gmail.com">
</div>
)
}
カスタムコンポーネントを渡すこともできます。
function Dashboard({ children }) {
return (
<div style={{ padding: '25px 12px' }}>
{children}
</div>
)
}
function App() {
return (
<div>
<MyComponent component={Dashboard} name="George" age={16} email="george@gmail.com">
</div>
)
}
2. Error Boundariesを使う(Use Error Boundaries)
JavaScriptでは、私達はほとんどのエラーをtry/catch
ー発生したエラーを"catch"できるコードのブロックー で対応しています。エラーがcatchブロックに補足された時、私達はアプリがクラッシュするのを避けることができます。
例はこのようなものです。
function getFromLocalStorage(key, value) {
try {
const data = window.localStorage.get(key)
return JSON.parse(data)
} catch (error) {
console.error
}
}
Reactは結局ただのJavaScriptなので、エラーをキャッチしてハンドルできると考えるでしょう。しかし、Reactの性質上、コンポーネント内でのJavScriptエラーは内部のstateを破壊し、将来のレンダーにおいてcryptic errors(不可解なエラー)を引き起こします。
このような理由で、ReactチームはError Boundariesを紹介しています。そして全てのReactデベロッパーがそれらを知っておくべきです。そうすれば自らのReactアプリで使用することができます。
Error Boundaries登場以前のエラー発生の問題点は、これらの不可解なエラーが発生した時、Reactはそれらに対処したり、リカバリーする方法を私達に与えていなかったことです。ですので、私達は皆Error Boundariesが必要なのです。
Error Boundariesは、Reactコンポーネントです。コンポーネントツリーのどこででもエラーをキャッチすることができ、ログ出力し、クラッシュしたコンポーネントツリーの代わりにフォールバック用UIを表示させることができます。Error Boundariesは、レンダー中、ライフサイクルメソッド内、そしてError Boundaries以下に存在する全体のツリーのコンストラクタ内でエラーをキャッチします(そしてこれが、私達のアプリのトップのどこかでError Boundariesを宣言しレンダーする理由です)。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo)
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>
}
return this.props.children
}
}
そして、普通のコンポーネントと同じように使用できます。
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
3. 前回の値を保持する(Retain Previous Values)
propsやstateを更新している時、あなたはReact.useRef
を使ってそれらの前回の値を保持することができます。
例えば、1つの配列のアイテムの、現在と前回の変更を追跡するために、前回の値が割り当てられるReact.useRef
と、現在の値が割り振られるReact.useState
を使用することができます。
function MyComponent() {
const [names, setNames] = React.useState(['bob'])
const prevNamesRef = React.useRef([])
React.useEffect(() => {
prevNamesRef.current = names
})
const prevNames = prevNamesRef.current
return (
<div>
<h4>Current names:</h4>
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
<h4>Previous names:</h4>
<ul>
{prevNames.map((prevName) => (
<li key={prevName}>{prevName}</li>
))}
</ul>
</div>
)
}
これは正しく動作します。なぜならReact.useEffect
はレンダーが完了した後に実行されるからです。
setNames
が実行される時、コンポーネントは再度レンダーを行い、prefNamesRef
は前回の値を保持します。なぜなら、前回のレンダーから見てReact.useEffect
が最後に実行されるコードだからです。
そしてuseEffect
内でprevNamesRef.current
に値が再び代入されるので、次回のレンダー時には前回のnamesの値を保持します。
4. 値チェックにReact.useRefを使う(Use React.useRef for flexible non-stale value checks)
React Hooksが登場する以前、ComponentがDOMにマウントされた後に、dataのフェッチなどの処理を行いたい場合、私達はcomponentDidMount
というクラスコンポーネントのStaticメソッドを使用していました。
React Hooksが発表された時、それはすぐにコンポーネントを書く最も人気の手段となりました。コンポーネントがアンマウントされた後にstateがセットされるのを防ぐために、コンポーネントがマウントされたかを監視したい時、私達はこのようにするでしょう。
import React from 'react'
import axios from 'axios'
class MyComponent extends React.Component {
mounted = false
state = {
frogs: [],
error: null,
}
componentDidMount() {
this.mounted = true
}
componentWillUnmount() {
this.mounted = false
}
async fetchFrogs = (params) => {
try {
const response = await axios.get('https://some-frogs-api.com/v1/', { params })
if (this.mounted) {
this.setState({ frogs: response.data.items })
}
} catch (error) {
if (this.mounted) {
this.setState({ error })
}
}
}
render() {
return (
<div>
<h4>Frogs:</h4>
<ul>
{this.state.frogs.map((frog) => <li key={frog.name}>{frog.name}</li>
)}
</ul>
</div>
)
}
}
React Hooksに統合後、HooksはcomponentDidMount
を持っていません。そして、アンマウント後に起こったstateアップデートによるメモリリークのコンセプトは未だにhooksに当てはまります。しかし、React Hooksを使ったcomponentDidMount
に似た方法はReact.useEffect
を使用することです。なぜなら、それはコンポーネントがレンダリングされた後に実行されるからです。もしあなたがReact.useRef
を使って、マウントされた値を割り当てるなら、以下のクラスコンポーネントの例で同じことができます。
import React from 'react'
import axios from 'axios'
function MyComponent() {
const [frogs, setFrogs] = React.useState([])
const [error, setError] = React.useState(null)
const mounted = React.useRef(false)
async function fetchFrogs(params) {
try {
const response = await axios.get('https://some-frogs-api.com/v1/', {
params,
})
if (mounted.current) {
setFrogs(response.data.items)
}
} catch (error) {
if (mounted.current) {
setError(error)
}
}
}
React.useEffect(() => {
mounted.current = true
return function cleanup() {
mounted.current = false
}
}, [])
return (
<div>
<h4>Frogs:</h4>
<ul>
{this.state.frogs.map((frog) => (
<li key={frog.name}>{frog.name}</li>
))}
</ul>
</div>
)
}
リレンダーをせず最新の変更をトラッキングするもう一つ別の例は、以下のように、React.useMemo
と併せて使用することです。(source)
function setRef(ref, value) {
// Using function callback version
if (typeof ref === 'function') {
ref(value)
// Using the React.useRef() version
} else if (ref) {
ref.current = value
}
}
function useForkRef(refA, refB) {
return React.useMemo(() => {
if (refA == null && refB == null) {
return null
}
return (refValue) => {
setRef(refA, refValue)
setRef(refB, refValue)
}
}, [refA, refB])
}
もしref propsが変更され、定義されたら、新たな関数を作ります。
つまり、Reactは古いフォークされたrefをnull、新しいフォークされたrefを現在のrefで呼ぶことを意味します。そしてReact.useMemo
が使われているので、refの変数たちはrefA
またはrefB
のref propsが変更されるまで記憶されます。ナチュラルなクリーンアップが起こるのです。
5. 他の要素に依存したカスタム要素にReact.useRefを使う(Use React.useRef for customizing elements that depend on other elements)
React.useRef
にはいくつか役に立つユースケースがあります。例えば、React.useRef
自体をref propsに割り当てるなどです。
function MyComponent() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
const nodeRef = React.useRef()
React.useEffect(() => {
const pos = nodeRef.current.getBoundingClientRect()
setPosition({
x: pos.x,
y: pos.y,
})
}, [])
return (
<div ref={nodeRef}>
<h2>Hello</h2>
</div>
)
}
もしdiv
要素の座標位置を取得したいなら、この例が役に立ちます。しかし、アプリケーションのどこかに存在する別の要素の位置をアップデートしたかったり、それに応じて条件ロジックを変更したり適応する場合、ベストな方法はref callback function pattern
を使うことです。callback function patternを使用すると、ReactコンポーネントまたはHTML DOM要素を第一引数として受け取ります。
下の例は、コールバック関数であるsetRef
がどこでref
propsに適応されるかを示すシンプルなものです。直接React.useRef
をDOM要素に割り当てるのとは対象的に、setRef
の内側では必要なことは何でもできるということがわかります:
const SomeComponent = function({ nodeRef }) {
const ownRef = React.useRef()
function setRef(e) {
if (e && nodeRef.current) {
const codeElementBounds = nodeRef.current.getBoundingClientRect()
// Log the <pre> element's position + size
console.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`)
ownRef.current = e
}
}
return (
<div
ref={setRef}
style={{ width: '100%', height: 100, background: 'green' }}
/>
)
}
function App() {
const [items, setItems] = React.useState([])
const nodeRef = React.useRef()
const addItems = React.useCallback(() => {
const itemNum = items.length
setItems((prevItems) => [
...prevItems,
{
[`item${itemNum}`]: `I am item # ${itemNum}'`,
},
])
}, [items, setItems])
return (
<div style={{ border: '1px solid teal', width: 500, margin: 'auto' }}>
<button type="button" onClick={addItems}>
Add Item
</button>
<SomeComponent nodeRef={nodeRef} />
<div ref={nodeRef}>
<pre>
<code>{JSON.stringify(items, null, 2)}</code>
</pre>
</div>
</div>
)
}
6. 高階コンポーネント(Higher Order Components)
プレインなJavaScriptで強力な再利用性を持つ関数を作るよくあるパターンは高階関数です。Reactも結局はJavaScriptなので、内部で高階関数を使用することができます。
再利用性のあるコンポーネントには、高階コンポーネントとというトリックが使えます。
高階コンポーネントとは、あなたがコンポーネントを引数にして、返り値としてコンポーネントを返す関数を持っているときのことです。高階関数がロジックを抽象化し、他の関数間で共有されるように、高階コンポーネントもコンポーネントをロジックから切り離し、他のコンポーネント間で共有することができます。つまり、たくさんの再利用可能なコンポーネントを採用し、アプリケーション内で実際に繰り返し使うことが可能だということなのです。
これは高階コンポーネントの例です。このスニペットでは、高階コンポーネントであるwithBorderはカスタムコンポーネントを引数として取り込み、隠されたミドルレイヤーコンポーネントを返します。そして、親がこの高階コンポーネントをレンダーするとき、これはコンポーネントとして呼ばれ、ミドルレイヤーコンポーネントからから渡されたpropsを受け取ります。
import React from 'react'
// Higher order component
const withBorder = (Component, customStyle) => {
class WithBorder extends React.Component {
render() {
const style = {
border: this.props.customStyle
? this.props.customStyle.border
: '3px solid teal',
}
return <Component style={style} {...this.props} />
}
}
return WithBorder
}
function MyComponent({ style, ...rest }) {
return (
<div style={style} {...rest}>
<h2>This is my component and I am expecting some styles.</h2>
</div>
)
}
export default withBorder(MyComponent, {
border: '4px solid teal',
})
7. Render Props
React内で使う私のお気に入りのトリックの一つは、render prop patternです。複数のコンポーネントでコードを共有するという問題を解決する点において、これは高階コンポーネントに似ています。Render propsは、レンダーに必要なものを全て差し戻すことが目的の関数をはっきりとさせます。
Reactでコンポーネントをレンダーする最もベーシックな方法は、このようなものでしょう。
function MyComponent() {
return <p>My component</p>
}
function App() {
const [fetching, setFetching] = React.useState(false)
const [fetched, setFetched] = React.useState(false)
const [fetchError, setFetchError] = React.useState(null)
const [frogs, setFrogs] = React.useState([])
React.useEffect(() => {
setFetching(true)
api
.fetchFrogs({ limit: 1000 })
.then((result) => {
setFrogs(result.data.items)
setFetched(true)
setFetching(false)
})
.catch((error) => {
setError(error)
setFetching(false)
})
}, [])
return (
<MyComponent
fetching={fetching}
fetched={fetched}
fetchError={fetchError}
frogs={frogs}
/>
)
}
Render Propsを使う場合は、その子をレンダーするpropは慣例的にrenderと呼ばれます。
function MyComponent({ render }) {
const [fetching, setFetching] = React.useState(false)
const [fetched, setFetched] = React.useState(false)
const [fetchError, setFetchError] = React.useState(null)
const [frogs, setFrogs] = React.useState([])
React.useEffect(() => {
setFetching(true)
api
.fetchFrogs({ limit: 1000 })
.then((result) => {
setFrogs(result.data.items)
setFetched(true)
setFetching(false)
})
.catch((error) => {
setError(error)
setFetching(false)
})
}, [])
return render({
fetching,
fetched,
fetchError,
frogs,
})
}
この例では、MyComponent
はrender prop componentとして参照するコンポーネントの例です。なぜなら、MyComponent
はpropとしてのrender
を期待していますし、子をレンダーするためのそれを実行するからです。
これはReactにおいて、とてもパワフルなパターンです。renderのコールバックを通して引数として、共有されたstateとdataを渡すことができるのです。複数のコンポーネントにおいて、そのコンポーネントはリレンダーされ、再利用されることが可能になります。
function App() {
return (
<MyComponent
render={({ fetching, fetched, fetchError, frogs }) => (
<div>
{fetching
? 'Fetching frogs...'
: fetched
? 'The frogs have been fetched!'
: fetchError
? `An error occurred while fetching the list of frogs: ${fetchError.message}`
: null}
<hr />
<ul
style={{
padding: 12,
}}
>
{frogs.map((frog) => (
<li key={frog.name}>
<div>Frog's name: {frog.name}</div>
<div>Frog's age: {frog.age}</div>
<div>Frog's gender: {frog.gender}</div>
</li>
))}
</ul>
</div>
)}
/>
)
}
8. Memoize
Reactデベロッパーとして知っておくべき最も大事なことの一つは、React.memo
のようなコンポーネントのパフォーマンス最適化です。それを知っておくと、無限ループのようなとてもひどいエラーを防ぐことができます。
Reactアプリにおいて、Memoization(メモ化)を行う、いくつかの方法があるので是非読んでみてください。
締め
ここでこの投稿は終わりです!この記事があなたにとって価値があることを願います。
Mediumも見てくださいね。
感想
まず、今回の記事は翻訳が難しかったです。英語もさることながら、自分のReact Hooksの理解が浅いなと実感しました。
逆にReact HooksやよりReactの理解が進めば、これらのトリックはとても役に立ちそうだと思いました。
間違い等見つけましたら教えていただけるとありがたいです。