以下は、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コンポーネントを作ることができます。
importReactfrom'react'constMyComponent='div'functionApp(){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を宣言できます。
functionMyComponent({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>)}
このように使用することができます。
functionApp(){return(<div><MyComponentcomponent="div"name="George"age={16}email="george@gmail.com"></div>
)
}
カスタムコンポーネントを渡すこともできます。
functionDashboard({children}){return(<divstyle={{padding:'25px 12px'}}>{children}</div>)}functionApp(){return(<div><MyComponentcomponent={Dashboard}name="George"age={16}email="george@gmail.com"></div>
)
}
2. Error Boundariesを使う(Use Error Boundaries)
JavaScriptでは、私達はほとんどのエラーをtry/catch
ー発生したエラーを"catch"できるコードのブロックー で対応しています。エラーがcatchブロックに補足された時、私達はアプリがクラッシュするのを避けることができます。
例はこのようなものです。
functiongetFromLocalStorage(key,value){try{constdata=window.localStorage.get(key)returnJSON.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を宣言しレンダーする理由です)。
classErrorBoundaryextendsReact.Component{constructor(props){super(props)this.state={hasError:false}}staticgetDerivedStateFromError(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 servicelogErrorToMyService(error,errorInfo)}render(){if(this.state.hasError){// You can render any custom fallback UIreturn<h1>Something went wrong.</h1>}returnthis.props.children}}
そして、普通のコンポーネントと同じように使用できます。
<ErrorBoundary><MyWidget/></ErrorBoundary>
3. 前回の値を保持する(Retain Previous Values)
propsやstateを更新している時、あなたはReact.useRef
を使ってそれらの前回の値を保持することができます。
例えば、1つの配列のアイテムの、現在と前回の変更を追跡するために、前回の値が割り当てられるReact.useRef
と、現在の値が割り振られるReact.useState
を使用することができます。
functionMyComponent(){const[names,setNames]=React.useState(['bob'])constprevNamesRef=React.useRef([])React.useEffect(()=>{prevNamesRef.current=names})constprevNames=prevNamesRef.currentreturn(<div><h4>Current names:</h4><ul>{names.map((name)=>(<likey={name}>{name}</li>))}</ul><h4>Previous names:</h4><ul>{prevNames.map((prevName)=>(<likey={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がセットされるのを防ぐために、コンポーネントがマウントされたかを監視したい時、私達はこのようにするでしょう。
importReactfrom'react'importaxiosfrom'axios'classMyComponentextendsReact.Component{mounted=falsestate={frogs:[],error:null,}componentDidMount(){this.mounted=true}componentWillUnmount(){this.mounted=false}asyncfetchFrogs=(params)=>{try{constresponse=awaitaxios.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)=><likey={frog.name}>{frog.name}</li>)}</ul></div>)}}
React Hooksに統合後、HooksはcomponentDidMount
を持っていません。そして、アンマウント後に起こったstateアップデートによるメモリリークのコンセプトは未だにhooksに当てはまります。しかし、React Hooksを使ったcomponentDidMount
に似た方法はReact.useEffect
を使用することです。なぜなら、それはコンポーネントがレンダリングされた後に実行されるからです。もしあなたがReact.useRef
を使って、マウントされた値を割り当てるなら、以下のクラスコンポーネントの例で同じことができます。
importReactfrom'react'importaxiosfrom'axios'functionMyComponent(){const[frogs,setFrogs]=React.useState([])const[error,setError]=React.useState(null)constmounted=React.useRef(false)asyncfunctionfetchFrogs(params){try{constresponse=awaitaxios.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=truereturnfunctioncleanup(){mounted.current=false}},[])return(<div><h4>Frogs:</h4><ul>{this.state.frogs.map((frog)=>(<likey={frog.name}>{frog.name}</li>))}</ul></div>)}
リレンダーをせず最新の変更をトラッキングするもう一つ別の例は、以下のように、React.useMemo
と併せて使用することです。(source)
functionsetRef(ref,value){// Using function callback versionif(typeofref==='function'){ref(value)// Using the React.useRef() version}elseif(ref){ref.current=value}}functionuseForkRef(refA,refB){returnReact.useMemo(()=>{if(refA==null&&refB==null){returnnull}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に割り当てるなどです。
functionMyComponent(){const[position,setPosition]=React.useState({x:0,y:0})constnodeRef=React.useRef()React.useEffect(()=>{constpos=nodeRef.current.getBoundingClientRect()setPosition({x:pos.x,y:pos.y,})},[])return(<divref={nodeRef}><h2>Hello</h2></div>)}
もしdiv
要素の座標位置を取得したいなら、この例が役に立ちます。しかし、アプリケーションのどこかに存在する別の要素の位置をアップデートしたかったり、それに応じて条件ロジックを変更したり適応する場合、ベストな方法はref callback function pattern
を使うことです。callback function patternを使用すると、ReactコンポーネントまたはHTML DOM要素を第一引数として受け取ります。
下の例は、コールバック関数であるsetRef
がどこでref
propsに適応されるかを示すシンプルなものです。直接React.useRef
をDOM要素に割り当てるのとは対象的に、setRef
の内側では必要なことは何でもできるということがわかります:
constSomeComponent=function({nodeRef}){constownRef=React.useRef()functionsetRef(e){if(e&&nodeRef.current){constcodeElementBounds=nodeRef.current.getBoundingClientRect()// Log the <pre> element's position + sizeconsole.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`)ownRef.current=e}}return(<divref={setRef}style={{width:'100%',height:100,background:'green'}}/>)}functionApp(){const[items,setItems]=React.useState([])constnodeRef=React.useRef()constaddItems=React.useCallback(()=>{constitemNum=items.lengthsetItems((prevItems)=>[...prevItems,{[`item${itemNum}`]:`I am item # ${itemNum}'`,},])},[items,setItems])return(<divstyle={{border:'1px solid teal',width:500,margin:'auto'}}><buttontype="button"onClick={addItems}>
Add Item
</button><SomeComponentnodeRef={nodeRef}/><divref={nodeRef}><pre><code>{JSON.stringify(items,null,2)}</code></pre></div></div>)}
6. 高階コンポーネント(Higher Order Components)
プレインなJavaScriptで強力な再利用性を持つ関数を作るよくあるパターンは高階関数です。Reactも結局はJavaScriptなので、内部で高階関数を使用することができます。
再利用性のあるコンポーネントには、高階コンポーネントとというトリックが使えます。
高階コンポーネントとは、あなたがコンポーネントを引数にして、返り値としてコンポーネントを返す関数を持っているときのことです。高階関数がロジックを抽象化し、他の関数間で共有されるように、高階コンポーネントもコンポーネントをロジックから切り離し、他のコンポーネント間で共有することができます。つまり、たくさんの再利用可能なコンポーネントを採用し、アプリケーション内で実際に繰り返し使うことが可能だということなのです。
これは高階コンポーネントの例です。このスニペットでは、高階コンポーネントであるwithBorderはカスタムコンポーネントを引数として取り込み、隠されたミドルレイヤーコンポーネントを返します。そして、親がこの高階コンポーネントをレンダーするとき、これはコンポーネントとして呼ばれ、ミドルレイヤーコンポーネントからから渡されたpropsを受け取ります。
importReactfrom'react'// Higher order componentconstwithBorder=(Component,customStyle)=>{classWithBorderextendsReact.Component{render(){conststyle={border:this.props.customStyle?this.props.customStyle.border:'3px solid teal',}return<Componentstyle={style}{...this.props}/>}}returnWithBorder}functionMyComponent({style,...rest}){return(<divstyle={style}{...rest}><h2>This is my component and I am expecting some styles.</h2></div>)}exportdefaultwithBorder(MyComponent,{border:'4px solid teal',})
7. Render Props
React内で使う私のお気に入りのトリックの一つは、render prop patternです。複数のコンポーネントでコードを共有するという問題を解決する点において、これは高階コンポーネントに似ています。Render propsは、レンダーに必要なものを全て差し戻すことが目的の関数をはっきりとさせます。
Render props expose a function who's purpose is to pass back everything the outside world needs to render its children.
Reactでコンポーネントをレンダーする最もベーシックな方法は、このようなものでしょう。
functionMyComponent(){return<p>My component</p>}functionApp(){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(<MyComponentfetching={fetching}fetched={fetched}fetchError={fetchError}frogs={frogs}/>)}
Render Propsを使う場合は、その子をレンダーするpropは慣例的にrenderと呼ばれます。
functionMyComponent({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)})},[])returnrender({fetching,fetched,fetchError,frogs,})}
この例では、MyComponent
はrender prop componentとして参照するコンポーネントの例です。なぜなら、MyComponent
はpropとしてのrender
を期待していますし、子をレンダーするためのそれを実行するからです。
これはReactにおいて、とてもパワフルなパターンです。renderのコールバックを通して引数として、共有されたstateとdataを渡すことができるのです。複数のコンポーネントにおいて、そのコンポーネントはリレンダーされ、再利用されることが可能になります。
functionApp(){return(<MyComponentrender={({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/><ulstyle={{padding:12,}}>{frogs.map((frog)=>(<likey={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の理解が進めば、これらのトリックはとても役に立ちそうだと思いました。
間違い等見つけましたら教えていただけるとありがたいです。