2020/12/04
Reactの状態管理ライブラリー「Recoil」で非同期通信によるデータ読み込み
前回の記事でReactの状態管理ライブラリーの「Recoil」について、公式のチュートリアルを元に少し触れてみました。
RecoilによってReactでネックの一つとなる状態管理が便利になることがわかりましたが、
今回はさらにRecoilの売りの一つである非同期通信によるデータの扱いについて触れてみました。
実際のWEBアプリでは、ベースとなるデータや保存されたデータを予め読み込んで表示することがほとんどです。
そのため、状態管理においても、非同期通信をともなうデータの処理が要になってきます。
前回のデモでTodoリストを作りましたが、初期では何もデータがない状態でした。
今回は、初期状態として非同期通信により情報を読み込み、表示する方法を試してみました。
↓作ってみたもの
DEMO
方法
前回はTodoListの初期値をからの配列として設定していましたが、
非同期通信で読み込んだ値を初期値に利用したい場合、
atom() の default に selector() による処理を指定します。
初期値を静的に指定する場合
1 2 3 4 | const todoListState = atom({ key: 'todoListState', default: [] }); |
初期値を非同期通信で取得したデータにする場合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | const todoListState = atom({ key: 'todoListState', default: selector({ key: 'savedTodoListState', get: async ({get}) => { try { const response = await axios('savedTodoList.json'); let list: TodoListType[] = response.data.map((item: SavedTodoListType) => { id = item.id; return { id: item.id, text: item.text, createAt: new Date(item.createAt), completedAt: item.completedAt ? new Date(item.completedAt) : undefined, isComplete: item.isComplete } }); return list; } catch (error) { throw error; } }, }), }); |
この例では、selector() 内で axios で取得したデータを一部処理して atom() の default に返す処理をしています。
ポイントは、selector() の get の関数を async をつけた非同期関数としているところです。
また、非同期通信にはエラーがつきものなので、try catch によるエラーの例外処理を行っています。
読込中の出し分け
1 2 3 4 5 6 7 8 9 10 11 12 13 | function App() { return ( <RecoilRoot> <ErrorBoundary> <main className="mainContent"> <React.Suspense fallback={<div>Loading...</div>}> <TodoList /> </React.Suspense> </main> </ErrorBoundary> </RecoilRoot> ); } |
<TodoList />を<React.Suspence />で囲み、fallbackに読込中の要素を指定することで、
通信前の状態を簡単に指定することができます。
また、<ErrorBoundary />を定義し、エラーが起こった場合の表示も準備しておくことで、例外の表示にも対応しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | interface Props { children: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // You can also log the error to an error reporting service console.log("error: " + error); console.log("errorInfo: " + JSON.stringify(errorInfo)); console.log("componentStack: " + errorInfo.componentStack); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } |
表示だ仕分けのために新たに状態管理を追加する必要がないので、
わかりやすくていいなと個人的には感じました。
参考にさせていただいたサイト
Asynchronous Data Queries | Recoil
2010年代も終わるのでaxiosについて確認しておきたい – Qiita
Reactアプリにおける非同期通信エラー処理の実装案 | Hypertext Candy
Error Boundaries | React TypeScript Cheatsheets
reactjs – How to properly type a React ErrorBoundary class component in Typescript? – Stack Overflow
今回のDEMO App.tsx のコード全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | import React, {useState, ErrorInfo, ReactNode} from 'react'; import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, useSetRecoilState, } from 'recoil'; import axios from 'axios'; import './App.scss'; type TodoListType = { id: number, text: string, createAt: Date, completedAt?: Date, isComplete: boolean } type SavedTodoListType = { id: number, text: string, createAt: string, completedAt?: string, isComplete: boolean } let id = 0; interface Props { children: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // You can also log the error to an error reporting service console.log("error: " + error); console.log("errorInfo: " + JSON.stringify(errorInfo)); console.log("componentStack: " + errorInfo.componentStack); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } // const defaultTodoListState:TodoListType[] = []; const todoListState = atom({ key: 'todoListState', default: selector({ key: 'savedTodoListState', get: async ({get}) => { try { const response = await axios('savedTodoList.json'); let list: TodoListType[] = response.data.map((item: SavedTodoListType) => { id = item.id; return { id: item.id, text: item.text, createAt: new Date(item.createAt), completedAt: item.completedAt ? new Date(item.completedAt) : undefined, isComplete: item.isComplete } }); return list; } catch (error) { throw error; } }, }), }); const todoListFilterState = atom({ key: 'todoListFilterState', default: 'Show All', }); const filteredTodoListState = selector({ key: 'filteredTodoListState', get: ({get}) => { const filter = get(todoListFilterState); const list = get(todoListState); switch (filter) { case 'Show Completed': return list.filter((item) => item.isComplete); case 'Show Uncompleted': return list.filter((item) => !item.isComplete); default: return list; } }, }); function TodoList() { const todoList = useRecoilValue(filteredTodoListState); return ( <div className="todoList"> <TodoListStats /> <TodoListFilters /> <TodoItemCreator /> <ul className="todoList__list"> {todoList.map((todoItem) => ( <TodoItem key={todoItem.id} item={todoItem} /> ))} </ul> </div> ); } function TodoItemCreator() { const [inputValue, setInputValue] = useState(''); const setTodoList = useSetRecoilState(todoListState); const addItem = () => { setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text: inputValue, createAt: new Date(), isComplete: false, }, ]); setInputValue(''); }; const onChange = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => { setInputValue(value); }; return ( <p className="todoListCreator"> <input type="text" value={inputValue} onChange={onChange} /> <button onClick={addItem}>Add</button> </p> ); } function getId() { return id++; } function TodoItem({item}: { key: number, item: TodoListType }) { const [todoList, setTodoList] = useRecoilState(todoListState); const index = todoList.findIndex((listItem) => listItem === item); const editItemText = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); }; const toggleItemCompletion = () => { const newList = replaceItemAtIndex(todoList, index, { ...item, completedAt: !item.isComplete ? new Date() : undefined, isComplete: !item.isComplete, }); setTodoList(newList); }; const deleteItem = () => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }; return ( <li className="todoList__item"> <input type="text" value={item.text} onChange={editItemText} /> <input type="text" size={ 8 } readOnly value={ ( '00' + (item.createAt as Date).getHours() ).slice( -2 ) + ':' + ( '00' + (item.createAt as Date).getMinutes() ).slice( -2 ) + ':' + ( '00' + (item.createAt as Date).getSeconds() ).slice( -2 ) } /> <input type="checkbox" checked={item.isComplete} onChange={toggleItemCompletion} /> <input type="text" size={ 8 } readOnly value={ typeof item.completedAt != 'undefined' ? ( '00' + (item.completedAt as Date).getHours() ).slice( -2 ) + ':' + ( '00' + (item.completedAt as Date).getMinutes() ).slice( -2 ) + ':' + ( '00' + (item.completedAt as Date).getSeconds() ).slice( -2 ) : '' } /> <button className="delete" onClick={deleteItem}>X</button> </li> ); } function replaceItemAtIndex(todoList: TodoListType[], index: number, newValue: TodoListType) { return [...todoList.slice(0, index), newValue, ...todoList.slice(index + 1)]; } function removeItemAtIndex(todoList: TodoListType[], index: number) { return [...todoList.slice(0, index), ...todoList.slice(index + 1)]; } function TodoListFilters() { const [filter, setFilter] = useRecoilState(todoListFilterState); const updateFilter = ({target: {value}}: React.ChangeEvent<HTMLSelectElement>) => { setFilter(value); }; return ( <p className="todoListFilter"> <span className="todoListFilter__label">Filter:</span> <select value={filter} onChange={updateFilter}> <option value="Show All">All</option> <option value="Show Completed">Completed</option> <option value="Show Uncompleted">Uncompleted</option> </select> </p> ); } const todoListStatsState = selector({ key: 'todoListStatsState', get: ({get}) => { const todoList = get(todoListState); const totalNum = todoList.length; const totalCompletedNum = todoList.filter((item) => item.isComplete).length; const totalUncompletedNum = totalNum - totalCompletedNum; const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, }; }, }); function TodoListStats() { const { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, } = useRecoilValue(todoListStatsState); const formattedPercentCompleted = Math.round(percentCompleted * 100); return ( <ul className="todoListStats"> <li>Total items: <span className="todoListStats__value">{totalNum}</span></li> <li>Items completed: <span className="todoListStats__value">{totalCompletedNum}</span></li> <li>Items not completed: <span className="todoListStats__value">{totalUncompletedNum}</span></li> <li>Percent completed: <span className="todoListStats__value">{formattedPercentCompleted}%</span></li> </ul> ); } function App() { return ( <RecoilRoot> <ErrorBoundary> <main className="mainContent"> <React.Suspense fallback={<div>Loading...</div>}> <TodoList /> </React.Suspense> </main> </ErrorBoundary> </RecoilRoot> ); } export default App; |
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE