# 리코일?
## React 내부 상태 관리의 한계
- 컴포넌트들 사이에서 state를 공유하려면 공통의 조상으로 끌어올려야하는데 그러면 리렌더링을 할 필요가 없는 것도 리렌더링을 해야할 수 있다.
- `React.memo`를 사용해서 리렌더링을 막을 수는 있다.
- `context`는 하나의 value만 저장할 수 있다. 각 consumer에서 사용되는 value들의 집합을 가질 수는 없다.
- 위 두 개의 문제는 state가 있는 곳과 state가 사용되는 곳 사이의 코드 분리가 어렵다.
## 그냥 context api를 사용하면?
context는 낮은 빈도의 업데이트에 사용하면 좋다.
![](https://i.imgur.com/5xx16dP.png)
위와 같은 서비스가 있다고 가정해보자. 이미지 목록이 있고 오른쪽에는 이미지 목록에서 이미지를 클릭하면 메타데이터를 보여주는 컴포넌트를 렌더링한다. 서비스에서는 이미지의 이름을 변경할 수 있는데 이 때 이미지 컴포넌트와 메타데이터 컴포넌트만 다시 렌더링을 하는 것이 좋다. **하지만 context는 Provider 하위의 모든 consumer들이 provider 속성이 변경될 때마다 다시 렌더링이 되기 때문에 일부만 다시 렌더링이 되게 하기가 어렵다. **
## 리코일의 장점
- 리코일은 배우기 쉽다. hook을 사용하고 있는 사람들에게 익숙하다.
- 컴포넌트가 사용하는 데이터 조각만 사용할 수 있다.
- 계산된 selector를 선언할 수 있다.
- 비동기 데이터 흐름을 위한 내장 솔루션을 제공한다.
## Recoil의 기본 개념
recoil에서 데이터의 흐름은 `atome`에서 `selector`, 리액트 컴포넌트로 이어진다. `atom`은 컴포넌트가 구독할 수 있는 state의 기본 유닛이다. `selector`는 state를 동기적으로 또는 비동기적으로 transform 한다.
## 리코일 기본
## RecoilRoot
recoil state를 사용하는 컴포넌트의 부모 컴포넌트의 어디엔가 `RecoilRoot`를 적용해야한다.
### Atom
Atom은 컴포넌트가 구독할 수 있는 react state라고 볼 수 있다. atom의 값을 변경하면 구독하고 있는 컴포넌트들이 모두 다시 렌더링된다. 런타임에 생성이 될 수도 있다.
atom은 고유한 key 값이 필요하다.
atom을 생성할 때는 고유한 키 값과 default 값을 설정한다.
```javascript=
export const nameState = atom({
key: 'nameState',
default: 'Jane Doe'
});
```
### useRecoilState
atom의 값을 구독하여 업데이트할 수 있는 hook. `useState와` 동일한 방식으로 사용한다.
```javascript=
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
```
### useRecoilValue
setter 없이 atom의 값만을 반환한다.
### useSetRecoilState
setter 함수만들 반환한다.
```javascript=
import {nameState} from './someplace'
// useRecoilState
const NameInput = () => {
const [name, setName] = useRecoilState(nameState);
const onChange = (event) => {
setName(event.target.value);
};
return <>
<input type="text" value={name} onChange={onChange} />
<div>Name: {name}</div>
</>;
}
// useRecoilValue
const SomeOtherComponentWithName = () => {
const name = useRecoilValue(nameState);
return <div>{name}</div>;
}
// useSetRecoilState
const SomeOtherComponentThatSetsName = () => {
const setName = useSetRecoilState(nameState);
return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}
```
### selector
selector는 상태에서 파생된 데이터로 다른 atom에 의존하는 동적인 데이터를 만들 수 있게한다. `selector`는 다른 `atom`이나 `selector`를 input으로 받는 순수함수이다. 만약 이 input으로 받은 `atom`이나 `selector`가 업데이트 되면 `selector`가 re-evaluated된다.
컴포넌트에서 `atom`을 구독하는 것처럼 `selector`를 구독할 수 있다. `selector`를 만들어 recoil에서는 최소한의 state 집합만 atom에 저장이 되게 해서 불필요한 state를 줄일 수 있다.
```javascript=
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
```
`get` 프로퍼티는 computed 되는 함수이다. 매개변수로 `get`이라는 값을 받는데 이것을 이용해서 다른 `atom`이나 `selector`에 dependency를 만들 수 있다.
`set`이 없으면 writable 하지 않기 때문에 `useRecoilValue()`를 사용해서 값을 읽는다.
### atomFamily
atomFamily는 atom과 동일하지만 다른 인스턴스와 구분이 가능한 매개변수를 받는다.
```javascript=
// store의 코드
const getImage = async id => {
return new Promise(resolve => {
const url = `http://someplace.com/${id}.png`;
let image = new Image();
image.onload = () =>
resolve({
id,
name: `Image ${id}`,
url,
metadata: {
width: `${image.width}px`,
height: `${image.height}px`
}
});
image.src = url;
});
};
export const imageState = atomFamily({
key: "imageState",
default: async id => getImage(id)
});
// 컴포넌트의 코드
const Images = () => {
const imageList = useRecoilValue(imageListState);
return (
<div className="images">
{imageList.map(id => (
<Suspense key={id} fallback="Loading...">
<Image id={id} />
</Suspense>
))}
</div>
);
};
// 단일 이미지
const Image = ({ id }) => {
const { name, url } = useRecoilValue(imageState(id));
return (
<div className="image">
<div className="name">{name}</div>
<img src={url} alt={name} />
</div>
);
};
```
## Asynchronous Data Query
리코일은 state와 derived state를 리액트 컴포넌트에 data-flow 그래프를 통해서 연결하는 방식을 제공한다. 그런데 그래프의 함수가 asynchronous일 수도 있다는 점에서 매우 강력하다. 이것을 통해서 동기식의 리액트 컴포넌트 render 함수에서 비동기 함수를 사용하기가 쉬워진다. 리코일은 `selector`에서 동기 함수와 비동기 함수를 섞어서 사용하기 용이하게 해준다. `selector`의 `get` callback에서 값이 아니라 promise를 반환해서 interface를 동일하게 유지할 수 있다.
`selector`는 비동기적인 데이터를 리코일의 data-flow graph에 연결하는데 사용된다. `selector`는 주어진 input이 있으면 항상 같은 결과를 반환한다. 이것은 `selector`의 evaluation이 캐싱되고 재실행되고 여러번 사용될 수 있다는 점에서 중요하다. 이러한 점 때문에 `selector`는 read-only DB query를 작성하는데 좋은 방법이 된다. 변할 수 있는 데이터에서는 **Query Refresh**를 사용할 수 있고 synchronize mutable state나 persist state, 다른 side-effect에 대해서는 **Atom Effects** api를 고려할 수 있다.
### asynchronous 예제
```jsx=
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
```
`selector`를 사용하는 컴포넌트 입장에서는 `selector` 내부가 비동기인지 아닌지를 알 필요가 없다. 하지만 리액트의 render 함수가 동기적이기 때문에 promise가 resolve 되기 전의 처리를 해줄 필요가 있다.
#### React Suspense 사용.
`React Suspense`를 사용하면 자식들 중에 pending 상태인 것을 catch 할 수 있다
만약 에러가 있는 경우에는 `<ErrorBoundary>`를 사용해서 에러를 잡을 수 있다.
만약 `React Suspense`를 사용하고 싶지 않은 경우에는 `useRecoilValueLoadable`을 사용할 수 있다.
### Queries with Parameters
만약 query를 할 때 derived state 외에도 매개변수를 받아서 query를 하고 싶으면 `selectorFamily`를 사용할 수 있다.
```jsx=
const userNameQuery = selectorFamily({
key: 'UserName',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<UserInfo userID={1}/>
<UserInfo userID={2}/>
<UserInfo userID={3}/>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
```
`selector`로 query를 할 때 state, derived state, query를 섞어서 data-flow graph를 만들 수 있다. 이 그래프는 state가 업데이트 되면 자동으로 업데이트 되고 컴포넌트를 리렌더링 시킨다.
### Pre-fetching
성능을 위해서 렌더링 이전에 fetching을 시작하고 싶은 경우에 다음과 같이 할 수 있다.
```jsx=
function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const changeUser = useRecoilCallback(({snapshot, set}) => userID => {
snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info
set(currentUserIDState, userID); // change current user to start new render
});
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
);
}
```
### Query Default Atom Values
`atom`이 local state를 표현하지만 default value를 `selector`를 이용해서 query 하는 것은 매우 많이 사용하는 패턴이다.
```jsx=
const currentUserIDState = atom({
key: 'CurrentUserID',
default: selector({
key: 'CurrentUserID/Default',
get: () => myFetchCurrentUserID(),
}),
});
```
### Query Refresh
`selector`를 이용해서 데이터를 가져올 때 같은 state에 대해서 `selector` evaluation은 같은 결과를 준다는 것을 기억해야한다. 결과는 캐싱 되어 여러번 실행되어도 같은 값을 반환한다. 이 말은 어플리케이션의 life time 동안에 result가 변화하는 query에는 하나의 `selector`를 사용하면 안된다는 의미가 된다.
mutable 데이터에 사용할 수 있는 패턴은 몇 가지가 있다.
#### Request Id 사용
같은 input에 대해서 같은 `selector` evaluation을 반환하기 때문에 request id를 dependency로 추가해서 보내는 방법이 있다.
```jsx=
const userInfoQueryRequestIDState = atomFamily({
key: 'UserInfoQueryRequestID',
default: 0,
});
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async ({get}) => {
get(userInfoQueryRequestIDState(userID)); // Add request ID as a dependency
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response;
},
});
function useRefreshUserInfo(userID) {
setUserInfoQueryRequestID = useSetRecoilState(userInfoQueryRequestIDState(userID));
return () => {
setUserInfoQueryRequestID(requestID => requestID + 1);
};
}
function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRefreshUserInfo(currentUserID);
return (
<div>
<h1>{currentUser.name}</h1>
<button onClick={refreshUserInfo}>Refresh</button>
</div>
);
}
```
#### Atom 사용
다른 방법은 `selector` 대신에 `atom`을 사용하는 방법이 있다. `atom`은 현재 promise를 지원하지 않기 때문에 query refresh가 pending 상태일 때 React Suspense를 사용하지 못한다. 하지만 수동으로 result와 status를 객체 안에 넣어서 사용할 수도 있다.
## 참고 링크
https://ui.toast.com/weekly-pick/ko_20200616
https://recoiljs.org/docs/introduction/motivation/
###### tags: `tech sharing`