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