Recoil學習(三)---異步、同步

Recoil通過數據流圖將數據及衍生數據映射給組件,其真正強大之處是可異步,我們可以react組件中使用異步函數。Recoil允許我們在數據流圖的seletors中無縫混合使用同步和異步函數。從selector 的get回調中返回Promise對象,接口保持不變。

selectors可以用作將異步數據合并到recoil數據流圖中的一種方法。記住selectors是純函數:對于給定的一組輸入,它們應該總是產生相同的結果。這一點非常重要,因為選擇器計算可能會執(zhí)行一次或多次,可能會重新啟動,也可能會被緩存。因此,selectors是建模只讀DB查詢的好方法,這重復查詢可以提供一致的數據。如果您想同步本地和服務器狀態(tài),那么請參閱異步狀態(tài)同步狀態(tài)持久。

只讀數據

同步舉例

一個簡單的同步 atom 和 selector 來獲取user name.

const currentUserIDState = atom({
  key: 'CurrentUserID',
  default: 1,
});

const currentUserNameState = selector({
  key: 'CurrentUserName',
  get: ({get}) => {
    return tableOfUsers[get(currentUserIDState)].name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameState);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <CurrentUserInfo />
    </RecoilRoot>
  );
}

異步舉例

如果用戶名存儲在我們需要查詢的數據庫中,我們所需要做的就是返回一個Promise或使用異步函數。當依賴變化時 selector會被重新評估并執(zhí)行新的查詢,查詢結果會被緩存,因此對于每個唯一的輸入,查詢只執(zhí)行一次。

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的組件不需要關心它是由同步原子狀態(tài)、派生選擇器狀態(tài)還是異步查詢支持的。

但是,由于React呈現函數是同步的,所以在Promise解析之前它會呈現什么?Recoil設計與React Suspense一起處理待處理數據。用Suspense 邊界包裝組件,將捕獲任何仍然掛起的派生組件,并回退給UI呈現。

function MyApp() {
  return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
}

錯誤處理 <ErrorBoundary />

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>
  );
}

帶參查詢

有時希望能夠基于參數進行查詢,而不僅僅是基于派生狀態(tài)。例如,可能想要基于組件props進行查詢。你以使用selectorFamily helper來完成:

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>
  );
}

數據流圖

當狀態(tài)更新時,數據流圖將自動更新并重新呈現React組件。
下面的示例將呈現當前用戶的名稱及其好友列表。如果一個朋友的名字被點擊,他們將成為當前的用戶,名字和列表將自動更新。

const currentUserIDState = atom({
  key: 'CurrentUserID',
  default: null,
});

const userInfoQuery = selectorFamily({
  key: 'UserInfoQuery',
  get: userID => async () => {
    const response = await myDBQuery({userID});
    if (response.error) {
      throw response.error;
    }
    return response;
  },
});

const currentUserInfoQuery = selector({
  key: 'CurrentUserInfoQuery',
  get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
});

const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friends = [];
    for (const friendID of friendList) {
      const friendInfo = get(userInfoQuery(friendID));
      friends.push(friendInfo);
    }
    return friends;
  },
});

function CurrentUserInfo() {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);
  const setCurrentUserID = useSetRecoilState(currentUserIDState);
  return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>
        {friends.map(friend =>
          <li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
            {friend.name}
          </li>
        )}
      </ul>
    </div>
  );
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

在上面的示例中,friendsInfoQuery使用一個查詢來獲取每個朋友的信息。但是,通過在循環(huán)中這樣做,它們本質上是序列化的。如果查找速度快,也許沒問題,如果開銷較大,您可以使用并發(fā)helper程序,如waitForAll、waitForNone或waitForAny,以并行地運行它們或處理部分結果,它們接受數組和命名對象作為依賴。

const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friends = get(waitForAll(
      friendList.map(friendID => userInfoQuery(friendID))
    ));
    return friends;
  },
});

Pre-Fetching

出于性能考慮,您可能希望在呈現之前獲取數據,這樣查詢就可以在我們開始渲染的時候進行。React文檔給出了一些例子,這種模式也適用于Recoil。
我們改變上面的例子,當用戶點擊改變用戶的按鈕時,就開始獲取下一個用戶信息:

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>
  );
}

不使用React Suspense

沒有必要使用React Suspense來處理掛起的異步選擇器。你也可以使用useRecoilValueLoadable()鉤子來確定渲染期間的狀態(tài)。

function UserInfo({userID}) {
  const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
  switch (userNameLoadable.state) {
    case 'hasValue':
      return <div>{userNameLoadable.contents}</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      throw userNameLoadable.contents;
  }
}

讀寫數據

與服務器同步狀態(tài)

我們可以訂閱遠程狀態(tài)中的異步更改,并更新atom值以匹配。
可以在react useEffect 實現。

function CurrentUserIDSubscription() {
  const setCurrentUserID = useSetRecoilState(currentUserIDState);

  useEffect(() => {
    RemoteStateAPI.subscribeToCurrentUserID(setCurrentUserID);
    // Specify how to cleanup after this effect
    return function cleanup() {
      RemoteServerAPI.unsubscribeFromCurrentUserID(setCurrentUserID);
    };
  }, []);

  return null;
}

function MyApp() {
  return (
    <RecoilRoot>
      <CurrentUserIDSubscription />
      <CurrentUserInfo />
    </RecoilRoot>
  );
}

雙向同步

您還可以同步狀態(tài),以便在服務器上更新本地更改。注意,這是一個簡化的示例,請注意避免反饋循環(huán)。

function CurrentUserIDSubscription() {
  const [currentUserID, setCurrentUserID] = useRecoilState(currentUserIDState);
  const knownServerCurrentUserID = useRef(currentUserID);

  // Subscribe server changes to update atom state
  useEffect(() => {
    function handleUserChange(id) {
      knownServerCurrentUserID.current = id;
      setCurrentUserID(id);
    }

    RemoteStateAPI.subscribeToCurrentUserID(handleUserChange);
    // Specify how to cleanup after this effect
    return function cleanup() {
      RemoteServerAPI.unsubscribeFromCurrentUserID(handleUserChange);
    };
  }, [knownServerCurrentUserID]);

  // Subscribe atom changes to update server state
  useEffect(() => {
    if (currentUserID !== knownServerCurrentUserID.current) {
      knownServerCurrentID.current = currentUserID;
      RemoteServerAPI.updateCurrentUser(currentUserID);
    }
  }, [currentUserID, knownServerCurrentUserID.current]);

  return null;
}

帶參狀態(tài)同步

還可以使用atomFamily helper根據參數同步本地狀態(tài)。注意,這個示例鉤子的每次調用都將創(chuàng)建一個訂閱,因此要注意避免冗余使用。

const friendStatusState = atomFamily({
  key: 'Friend Status',
  default: 'offline',
});

function useFriendStatusSubscription(id) {
  const setStatus = useSetRecoilState(friendStatusState(id));

  useEffect(() => {
    RemoteStateAPI.subscribeToFriendStatus(id, setStatus);
    // Specify how to cleanup after this effect
    return function cleanup() {
      RemoteServerAPI.unsubscribeFromFriendStatus(id, setStatus);
    };
  }, []);
}

數據流圖

使用原子來表示遠態(tài)的一個優(yōu)點是,你可以使用它作為其他導出態(tài)的輸入。下面的示例將顯示基于當前服務器狀態(tài)的當前用戶和好友列表。如果服務器改變了當前用戶,它將重新呈現整個列表,如果它只改變了一個朋友的狀態(tài),那么只有該列表條目將被重新呈現。如果單擊列表項,它將在本地更改當前用戶并更新服務器狀態(tài)。

const userInfoQuery = selectorFamily({
  key: 'UserInfoQuery',
  get: userID => async ({get}) => {
    const response = await myDBQuery({userID});
    if (response.error) {
      throw response.error;
    }
    return response;
  },
});

const currentUserInfoQuery = selector({
  key: 'CurrentUserInfoQuery',
  get: ({get}) => get(userInfoQuery(get(currentUserIDState)),
});

const friendColorState = selectorFamily({
  key: 'FriendColor',
  get: friendID => ({get}) => {
    const [status] = useRecoilState(friendStatusState(friendID));
    return status === 'offline' ? 'red' : 'green';
  }
})

function FriendStatus({friendID}) {
  useFriendStatusSubscription(friendID);
  const [status] = useRecoilState(friendStatusState(friendID));
  const [color] = useRecoilState(friendColorState(friendID));
  const [friend] = useRecoilState(userInfoQuery(friendID));
  return (
    <div style={{color}}>
      Name: {friend.name}
      Status: {status}
    </div>
  );
}

function CurrentUserInfo() {
  const {name, friendList} = useRecoilValue(currentUserInfoQuery)
  const setCurrentUserID = useSetRecoilState(currentUserIDState);
  return (
    <div>
      <h1>{name}</h1>
      <ul>
        {friendList.map(friendID =>
          <li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
            <React.Suspense fallback={<div>Loading...</div>}>
              <FriendStatus friendID={friendID} />
            </React.Suspense>
          </li>
        )}
      </ul>
    </div>
  );
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserIDSubscription />
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容