https://recoiljs.org 의 내용을 타이핑 한것입니다.
user 이름을 얻기위한 간단한 동기 atom과 selector 예제
const currentuserIDState = atom({
key: 'CurrentUserID',
default : 1,
}):
const currentUserNameState = selector({
key :'CurrentserName',
get : ({get}) => {
return tableofUsers[get(currentUserIDState)].name;
},
});
function CurrentUserInfo(){
const userName = useRocilValue(currentUserNameState);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RocoilRoot>
<CurrentUserInfo/>
</RecoilRoot>
);
}
만약 user의 이름을 쿼리해야하는데 DB에 저장되어있다면, Promise를 리턴하거나 async 함수를 사용하기만 하면 된다. 의존성에 하나라도 변경점이 생긴다면, selector는 새로운 쿼리를 재평가하고 다시 실행시킬 것이다. 그리고 결과는 쿼리가 유니크한 인풋이 있을 때에만 실행되도록 캐시된다.
const currentuserIDState = atom({
key: 'CurrentUserID',
default : 1,
}):
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를 사용하면서 동기 atom 상태나 파생된 selector 상태, 혹은 비동기 쿼리를 지원하는지 신경 쓰지 않아도 괜찮다.
또한 React Suspense를 사용하여 보류중인 데이터(promise가 resolve 되기 전) 에 특정 UI를 렌더 시킬수 있다.
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
Recoil selector는 컴포넌트에서 특정 값을 사용하려고 할 때 어떤 에러가 생길지에 대한 에러를 던질수 있다. 이는 React <ErrorBoundary>로 잡을수 있다. 참고로 React ERrorBoundary는 현재 Class 형만 지원을 하고 있다. hook을 사용해 하고 싶다면 react-error-boundary 패키지를 설치하자.
const currentUserNameQuery = selector({
key:'CurrentUserName',
get async ({get}) =>{
const response = await myDBQuery({
userID : get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo/>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
컴포넌트 props를 기반으로 쿼리를 하고 싶다고 가정해보자.
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로 모델리하면 상태와 파생된 상태, 그리고 쿼리를 혼합한 데이터 플로우 그래프를 만들수 있다. 이 그래프는 상태가 업데이트 되면 리액트 컴포넌트를 업데이트 하고 리렌더링한다.
아래 예시는 최근 유저의 이름과 그들의 친구 리스트를 렌더한다. 만약 친구 이름이 클릭 되면, 그 이름이 최근 유저가 되며 이름과 리스트는 자동적으로 업데이트 될것이다.
const currentUserIDState = atom({
key:'CurrentUserID',
default:null,
});
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: (userID) => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw reponse.error;
}
return resposne;
},
});
const currentUserInfoQuery = selector({
key: 'CurrentUserInfoQuery',
get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
});
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currnetUserInfoQuery);
return friendList.map((friendID) => get(userInfoQuery(friendID)));
},
});
function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const setCurrentUserID = useSetReocilState(currentUserIDState);
return (
<div>
<h1>{currentUser.name}</h1>
<ul>{friends.map(friend => (<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}> {friend.name} </li>))} </ul>
</div>
);
}
위의 예시에서, friendsInfoQuery는 쿼리를 이용해 각 친구에 대한 자료를 받아온다. 하지만 이를 루프하는것으로 기본적으로 직렬화 된다. 자원을 많이 사용한다면 waitForAll 과 같은 cocurrent helper를 사용하여 병렬로 돌릴수 있다.
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(waitForAll(friendList.map(friendId => userInfoQuery(friendID))),
);
return friends;
},
});
waitForNone 을 사용해 일부 데이터로 추가적인 UI 업데이트를 할 수 있다.
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friendLoadables = get(waitForNone(friendList.map(friendID => userInfoQuery(friendID))),
);
return friendLodables.filter(({state}) => state ==='hasValue').map(([contents}) => contents);
},
});
성능 문제로 인해 렌더링 이전에 받아오기를 시작하고 싶을수도 있다. 그 방법은 렌더링을 하면서 쿼리를 진행할 수 있다. 위의 예시를 사용자가 유저계정을 바꾸기 위해 버튼을 누르자마자 다음 유저 정보를 받아오기 시작하는 형태로 만들어보자.
function CurrnetUserInfo() {
const currentUse = userReocilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const changeUser = userReocilCallback(({snapshot, set}) => (userID) => {
snapshot.getLoadable(userInfoQuery(userID));
// 유저 정보 미리 가져오기
set(currentUserIDState, userID);
// 새로운 렌더시작하기 위해 현재 유저 변경하기
});
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map((friend) => (
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
))}
</ul>
</div>
);
}
보통 Atom 을 사용해 변경 가능한 로컬 상태를 나타내지만 , promise를 사용하여 default value를 설정할수도 있다.
const currentUserIDState = atom({
key: 'CurrentUserID',
default: myFetchCurrentUserId(),
});
또한 selector를 사용하여 쿼리를 정의 하거나 다른 state에 의존할수도 있다. selecotr를 atom default value로설정한다면, 그 default는 동적이고 해당 selector가 업데이트 될때마다 업데이트 될것이다.
const UserInfoState = atom({
key: 'UserInfo',
default: selector({
key: 'UserInfo/Default',
get: ({get}) => myFetchUserInfo(get(currentUserIDState)),
}),
});
또한 atomFamily로도 사용 될 수 있다.
const userInfoState = atomFamily({
key: 'UserInfo',
default: id => myFetchUserInfo(id),
});
const userInfoState = atomFamily({
key: 'UserInfo',
default: selectorFamily({
key:'UserInfo/Default',
get: id => ({get}) => myFetchUserInfo(id, get(paramState)),
}),
});
비동기 selecotr pending을 다루기 위해 React Suspense를 사용하는것은 필수가 아니다. useRecoilValueLoadable() 훅을 사용하여 렌더링 도중 현재 상태를 결정할 수 있다.
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadble.contents}</div>
case 'loading':
return <div>Loading...</div>
case 'hasError':
throw userNameLoadable.contents;
}
}
idempotent(멱등 : 연산을 여러번 적용하더라도 결과가 달라지지 않는 성질) 해야 한다. 쿼리를 갱신하거나 재시도하기 위해 다음과 같은 방법들을 사용할 수 있다.
useReocilRefresher_UNSTABLE() 훅은 selector의 모든 캐시를 제거하고 강제로 다시 selector를 재평가할 수 있게 하는 콜백 함수를 제공한다.
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
}
})
function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRecoilRefresher_UNSTABLE(userInfoQuery(currentUserID));
return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={() => refreshUserInfo()}>Refresh</button>
</div>
);
}
Selector 평가는 인풋을 바탕으로 주어진 상태에 일관된 값을 제공해야한다. 따라서 요청 ID를 패밀리 매개변수 혹은 쿼리에 대한 종속성으로 추가할 수 있다.
const userInfoQueryRequestIDState = atomFamily({
key: 'UserInfoQueryRequestID',
default: 0,
});
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: (userID) => async ({get}) => {
get(userInfoQueryRequestIDState(userID));
// request ID를 디펜던시로 추가
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response;
},
});
function useRefreshUserInfo(userId) {
setUserInfoQueryRequestID = useSetReocilState(userInfoQueryRequestIDState(userID),);
return () => {
setUserInfoQueryReuqestId(requestID => requestID +1);
};
}
function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRefreshUserInfo(currentUserID);
return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={refreshUserInfo}>Refresh</button>
</div>
);
}
selector 대신 atom을 사용해 쿼리 결과를 모델링하는것. Atom 상태를 새로운 쿼리 결과를 독자적인 새로고침 방침에 맞춰 명령적으로 업데이트 할 수 있다.
const userInfoState = atomFamily({
key:'UserInfo',
default: (userID0 => fetch(userInfoURL(userID)),
});
function RefreshUserInfo({userID}) {
const refreshUserInfo = useRecoilCallback(
({set}) => async (id) => {
const userInfo = await myDBQuery({userID});
set(userInfoState(userID), userInfo);
},
[userID],
);
useEffect(() => {
const intervalID = setInterval(refreshUserInfo, 1000);
return () => clearInterval(intervalID);
}, [refreshUserInfo]);
return null;
}
references : https://recoiljs.org/docs/guides/asynchronous-data-queries