반동으로 컨텍스트 마이그레이션
이유
오늘은 Create React App TypeScript Todo Example 2020 에서 Recoil로 컨텍스트를 마이그레이션했습니다.
프로젝트는 신기술 실험/평가를 돕기 위해 최신 기술 스택을 최대한 유지하는 개념입니다.
컨텍스트 코드
이전에는 간단한 컨텍스트 전역 저장소를 @laststance/use-app-state 사용했습니다.
코드는 여기에 있습니다.
이전에는 간단한 컨텍스트 전역 저장소를 @laststance/use-app-state 사용했습니다.
코드는 여기에 있습니다.
src/index.tsx
<Provider appState={} >
쪽import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Provider from '@laststance/use-app-state'
import './index.css'
import * as serviceWorker from './serviceWorker'
import App, { LocalStorageKey } from './App'
import ErrorBoundary from './ErrorBoundary'
import { NotFound } from './NotFound'
export type Routes = '/' | '/active' | '/completed'
export interface Todo {
id: string
bodyText: string
completed: boolean
}
export type TodoListType = Todo[]
export interface AppState {
todoList: TodoListType
}
const BlankAppState: AppState = {
todoList: [],
}
function LoadingAppStateFromLocalStorage(BlankAppState: AppState): AppState {
const stringifiedJSON: string | null = window.localStorage.getItem(
LocalStorageKey.APP_STATE
)
if (typeof stringifiedJSON === 'string') {
const Loaded: AppState = JSON.parse(stringifiedJSON)
return Loaded
}
return BlankAppState
}
const initialAppState = LoadingAppStateFromLocalStorage(BlankAppState)
interface Props {
path: Routes
}
const Controller: React.FC<Props> = ({ path }) => <App path={path} />
ReactDOM.render(
<ErrorBoundary>
<Provider initialState={initialAppState}>
<Router>
<Controller path="/" />
<Controller path="/active" />
<Controller path="/completed" />
<NotFound default />
</Router>
</Provider>
</ErrorBoundary>,
document.getElementById('root')
)
src/App/index.tsx
useAppState()
쪽import React, { useEffect } from 'react'
import { useAppState } from '@laststance/use-app-state'
import TodoTextInput from './TodoTextInput'
import TodoList from './TodoList'
import Menu from './Menu'
import Copyright from './Copyright'
import { Routes, AppState } from '../index'
import { RouteComponentProps } from '@reach/router'
import { Container } from './style'
export enum LocalStorageKey {
APP_STATE = 'APP_STATE',
}
interface Props {
path: Routes
}
const App: React.FC<Props & RouteComponentProps> = ({ path }) => {
const [appState] = useAppState<AppState>()
// if appState has changes, save it LocalStorage.
useEffect((): void => {
window.localStorage.setItem(
LocalStorageKey.APP_STATE,
JSON.stringify(appState) // convert JavaScript Object to string
)
}, [appState])
return (
<Container>
<section className="todoapp">
<TodoTextInput />
{appState.todoList.length ? (
<>
<TodoList path={path} />
<Menu path={path} />
</>
) : null}
</section>
<Copyright />
</Container>
)
}
export default App
컨텍스트를 구성하는 방법을 알고 싶다면 @laststance/use-app-state internal code directory like this 을 읽을 수 있습니다.
짧고 매우 간단합니다.
반동 코드
이 섹션은 위 항목에서 마이그레이션된 코드를 보여줍니다.
src/dataStructure.tsx는 리코일
atom
쪽을 만듭니다.import { atom, RecoilState } from 'recoil'
export type Routes = '/' | '/active' | '/completed'
export interface Todo {
id: string
bodyText: string
completed: boolean
}
export type TodoListType = Todo[]
export interface AppState {
todoList: TodoListType
}
export enum LocalStorageKey {
APP_STATE = 'APP_STATE',
}
function LoadAppStateFromLocalStorage(): AppState {
const stringifiedJSON: string | null = window.localStorage.getItem(
LocalStorageKey.APP_STATE
)
if (typeof stringifiedJSON === 'string') {
const Loaded: AppState = JSON.parse(stringifiedJSON)
return Loaded
}
const BlankAppState: AppState = {
todoList: [],
}
return BlankAppState
}
export const initialAppState: RecoilState<AppState> = atom({
key: 'initialAppState',
default: LoadAppStateFromLocalStorage(),
})
src/index.tsx
<RecoilRoot>
쪽import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import { RecoilRoot } from 'recoil'
import './index.css'
import * as serviceWorker from './serviceWorker'
import App from './App'
import ErrorBoundary from './ErrorBoundary'
import { NotFound } from './NotFound'
import { Routes } from './dataStructure'
interface Props {
path: Routes
}
const Controller: React.FC<Props> = ({ path }) => <App path={path} />
ReactDOM.render(
<ErrorBoundary>
<RecoilRoot>
<Router>
<Controller path="/" />
<Controller path="/active" />
<Controller path="/completed" />
<NotFound default />
</Router>
</RecoilRoot>
</ErrorBoundary>,
document.getElementById('root')
)
src/App/index.tsx
useRecoilValue()
쪽import React, { useEffect } from 'react'
import { useRecoilState } from 'recoil'
import NewTodoInput from './NewTodoInput'
import TodoList from './TodoList'
import UnderBar from './UnderBar'
import Copyright from './Copyright'
import { RouteComponentProps } from '@reach/router'
import { Layout } from './style'
import {
AppState,
initialAppState,
LocalStorageKey,
Routes,
} from '../dataStructure'
interface Props {
path: Routes
}
const App: React.FC<Props & RouteComponentProps> = ({ path }) => {
const [appState] = useRecoilState<AppState>(initialAppState)
// if appState has changes, save it LocalStorage.
useEffect((): void => {
window.localStorage.setItem(
LocalStorageKey.APP_STATE,
JSON.stringify(appState) // convert JavaScript Object to string
)
}, [appState])
return (
<Layout>
<section className="todoapp">
<NewTodoInput />
{appState.todoList.length ? (
<>
<TodoList path={path} />
<UnderBar path={path} />
</>
) : null}
</section>
<Copyright />
</Layout>
)
}
export default App
테스팅(β)
처음에는 jest.mock
에 의해 생성된 프로덕션 코드 recoilState 를 재정의하기 위해 atom()
방법을 시도했습니다.
그러나 jest.mock
방법은 jest.fn()
또는 mockImplementation()
으로 recoilState 생성 코드를 재정의하기 위해 recoilStateFactory 와 같은 약간 까다로운 구현이 필요합니다.
그래서 이렇게 간단한 Recoil State Setter 컴포넌트를 만들었습니다.
src/testUtils.tsx
import React, { useEffect } from 'react'
import { useRecoilState } from 'recoil'
import { AppState, initialAppState } from './dataStructure'
interface Props {
testEnvironmentInitialAppState?: AppState
}
export const InjectTestingRecoilState: React.FC<Props> = ({
testEnvironmentInitialAppState = {
todoList: [],
},
}) => {
const [, setAppState] = useRecoilState<AppState>(initialAppState)
useEffect(() => {
setAppState(testEnvironmentInitialAppState)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
테스트 코드에서 테스트 값을 Recoil State로 설정하는 데 사용합니다.
src/앱/TodoList/index.test.tsx
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import TodoList from './index'
import { RecoilRoot } from 'recoil'
import '@testing-library/jest-dom'
import { AppState } from '../../dataStructure'
import { InjectTestingRecoilState } from '../../testUtils'
const initialAppState: AppState = {
todoList: [
{
id: 'TsHx9eEN5Y4A',
bodyText: 'monster',
completed: false,
},
{
id: 'ba91OwrK0Dt8',
bodyText: 'boss black',
completed: false,
},
{
id: 'QwejYipEf5nk',
bodyText: 'caffe latte',
completed: false,
},
],
}
test('should be render 3 todo items in initialAppState', () => {
const screen = render(
<RecoilRoot>
<InjectTestingRecoilState
testEnvironmentInitialAppState={initialAppState}
/>
<TodoList path="/" />
</RecoilRoot>
)
expect(screen.getByTestId('todo-list')).toBeInTheDocument()
expect(screen.getByTestId('todo-list').children.length).toBe(3)
expect(Array.isArray(screen.getAllByTestId('todo-item'))).toBe(true)
expect(screen.getAllByTestId('todo-item')[0]).toHaveTextContent('monster')
expect(screen.getAllByTestId('todo-item')[1]).toHaveTextContent('boss black')
expect(screen.getAllByTestId('todo-item')[2]).toHaveTextContent('caffe latte')
})
test('should be work delete todo button', () => {
const screen = render(
<RecoilRoot>
<InjectTestingRecoilState
testEnvironmentInitialAppState={initialAppState}
/>
<TodoList path="/" />
</RecoilRoot>
)
// delete first item
fireEvent.click(screen.getAllByTestId('delete-todo-btn')[0])
// assertions
expect(screen.getByTestId('todo-list').children.length).toBe(2)
expect(Array.isArray(screen.getAllByTestId('todo-item'))).toBe(true)
expect(screen.getAllByTestId('todo-item')[0]).toHaveTextContent('boss black')
expect(screen.getAllByTestId('todo-item')[1]).toHaveTextContent('caffe latte')
})
test('should be work correctly all completed:true|false checkbox toggle button', () => {
const screen = render(
<RecoilRoot>
<InjectTestingRecoilState
testEnvironmentInitialAppState={initialAppState}
/>
<TodoList path="/" />
</RecoilRoot>
)
// toggle on
fireEvent.click(screen.getByTestId('toggle-all-btn'))
// should be completed all todo items
expect((screen.getAllByTestId('todo-item-complete-check')[0] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */
expect((screen.getAllByTestId('todo-item-complete-check')[1] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */
expect((screen.getAllByTestId('todo-item-complete-check')[2] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */
// toggle off
fireEvent.click(screen.getByTestId('toggle-all-btn'))
// should be not comleted all todo items
expect((screen.getAllByTestId('todo-item-complete-check')[0] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */
expect((screen.getAllByTestId('todo-item-complete-check')[1] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */
expect((screen.getAllByTestId('todo-item-complete-check')[2] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */
})
그러나 구현을 완료한 후 문서에서 다른 더 나은 솔루션을 찾았습니다.
<RecoilRoot>의 initializeState Props
Docs에 따르면 <RecoilRoot>
Recoil State 설정 기능을 Props로 받아들입니다.
이것은 Redux를 계속하고 구성 요소 테스트에 실제로 소수이기 때문에 기존<Provider />
패턴과 매우 유사합니다.
다음 작업으로 이 옵션을 시도해 보겠습니다.
이 게시물을 읽어 주셔서 감사합니다! 🤗
Reference
이 문제에 관하여(반동으로 컨텍스트 마이그레이션), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://dev.to/malloc007/migrate-context-to-recoil-40mc
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
Reference
이 문제에 관하여(반동으로 컨텍스트 마이그레이션), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/malloc007/migrate-context-to-recoil-40mc텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)