Recoil學(xué)習(xí)(三)---異步后豫、同步

Recoil通過數(shù)據(jù)流圖將數(shù)據(jù)及衍生數(shù)據(jù)映射給組件胁勺,其真正強(qiáng)大之處是可異步,我們可以react組件中使用異步函數(shù)怔昨。Recoil允許我們在數(shù)據(jù)流圖的seletors中無縫混合使用同步和異步函數(shù)雀久。從selector 的get回調(diào)中返回Promise對象,接口保持不變趁舀。

selectors可以用作將異步數(shù)據(jù)合并到recoil數(shù)據(jù)流圖中的一種方法赖捌。記住selectors是純函數(shù):對于給定的一組輸入,它們應(yīng)該總是產(chǎn)生相同的結(jié)果矮烹。這一點(diǎn)非常重要越庇,因?yàn)檫x擇器計(jì)算可能會執(zhí)行一次或多次,可能會重新啟動奉狈,也可能會被緩存卤唉。因此,selectors是建模只讀DB查詢的好方法仁期,這重復(fù)查詢可以提供一致的數(shù)據(jù)桑驱。如果您想同步本地和服務(wù)器狀態(tài),那么請參閱異步狀態(tài)同步狀態(tài)持久蟀拷。

只讀數(shù)據(jù)

同步舉例

一個簡單的同步 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>
  );
}

異步舉例

如果用戶名存儲在我們需要查詢的數(shù)據(jù)庫中碰纬,我們所需要做的就是返回一個Promise或使用異步函數(shù)。當(dāng)依賴變化時 selector會被重新評估并執(zhí)行新的查詢问芬,查詢結(jié)果會被緩存悦析,因此對于每個唯一的輸入,查詢只執(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的組件不需要關(guān)心它是由同步原子狀態(tài)、派生選擇器狀態(tài)還是異步查詢支持的挡鞍。

但是骑歹,由于React呈現(xiàn)函數(shù)是同步的,所以在Promise解析之前它會呈現(xiàn)什么?Recoil設(shè)計(jì)與React Suspense一起處理待處理數(shù)據(jù)墨微。用Suspense 邊界包裝組件道媚,將捕獲任何仍然掛起的派生組件,并回退給UI呈現(xiàn)翘县。

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

帶參查詢

有時希望能夠基于參數(shù)進(jìn)行查詢最域,而不僅僅是基于派生狀態(tài)。例如锈麸,可能想要基于組件props進(jìn)行查詢镀脂。你以使用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>
  );
}

數(shù)據(jù)流圖

當(dāng)狀態(tài)更新時,數(shù)據(jù)流圖將自動更新并重新呈現(xiàn)React組件忘伞。
下面的示例將呈現(xiàn)當(dāng)前用戶的名稱及其好友列表薄翅。如果一個朋友的名字被點(diǎn)擊沙兰,他們將成為當(dāng)前的用戶,名字和列表將自動更新翘魄。

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)中這樣做训措,它們本質(zhì)上是序列化的。如果查找速度快光羞,也許沒問題,如果開銷較大绩鸣,您可以使用并發(fā)helper程序,如waitForAll纱兑、waitForNone或waitForAny呀闻,以并行地運(yùn)行它們或處理部分結(jié)果,它們接受數(shù)組和命名對象作為依賴。

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

Pre-Fetching

出于性能考慮潜慎,您可能希望在呈現(xiàn)之前獲取數(shù)據(jù)捡多,這樣查詢就可以在我們開始渲染的時候進(jìn)行。React文檔給出了一些例子铐炫,這種模式也適用于Recoil垒手。
我們改變上面的例子,當(dāng)用戶點(diǎn)擊改變用戶的按鈕時倒信,就開始獲取下一個用戶信息:

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

讀寫數(shù)據(jù)

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

我們可以訂閱遠(yuǎn)程狀態(tài)中的異步更改鳖悠,并更新atom值以匹配榜掌。
可以在react useEffect 實(shí)現(xiàn)。

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)乘综,以便在服務(wù)器上更新本地更改憎账。注意,這是一個簡化的示例卡辰,請注意避免反饋循環(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根據(jù)參數(shù)同步本地狀態(tài)。注意九妈,這個示例鉤子的每次調(diào)用都將創(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);
    };
  }, []);
}

數(shù)據(jù)流圖

使用原子來表示遠(yuǎn)態(tài)的一個優(yōu)點(diǎn)是允蚣,你可以使用它作為其他導(dǎo)出態(tài)的輸入。下面的示例將顯示基于當(dāng)前服務(wù)器狀態(tài)的當(dāng)前用戶和好友列表呆贿。如果服務(wù)器改變了當(dāng)前用戶嚷兔,它將重新呈現(xiàn)整個列表森渐,如果它只改變了一個朋友的狀態(tài),那么只有該列表?xiàng)l目將被重新呈現(xiàn)冒晰。如果單擊列表項(xiàng)同衣,它將在本地更改當(dāng)前用戶并更新服務(wù)器狀態(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>
  );
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壶运,一起剝皮案震驚了整個濱河市耐齐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒋情,老刑警劉巖埠况,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棵癣,居然都是意外死亡辕翰,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門狈谊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喜命,“玉大人,你說我怎么就攤上這事河劝”陂牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵赎瞎,是天一觀的道長牌里。 經(jīng)常有香客問我,道長煎娇,這世上最難降的妖魔是什么二庵? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮缓呛,結(jié)果婚禮上催享,老公的妹妹穿的比我還像新娘。我一直安慰自己哟绊,他們只是感情好因妙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著票髓,像睡著了一般攀涵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上洽沟,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天以故,我揣著相機(jī)與錄音,去河邊找鬼裆操。 笑死怒详,一個胖子當(dāng)著我的面吹牛炉媒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昆烁,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼吊骤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了静尼?” 一聲冷哼從身側(cè)響起白粉,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鼠渺,沒想到半個月后鸭巴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡系冗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年奕扣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掌敬。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡惯豆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奔害,到底是詐尸還是另有隱情楷兽,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布华临,位于F島的核電站芯杀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雅潭。R本人自食惡果不足惜揭厚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扶供。 院中可真熱鬧筛圆,春花似錦、人聲如沸椿浓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扳碍。三九已至提岔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笋敞,已是汗流浹背碱蒙。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夯巷,地道東北人振亮。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓巧还,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坊秸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344