From 2b7b756212254271a36cffda85c968d24d0e4eb9 Mon Sep 17 00:00:00 2001
From: Fedor Katurov <gotham48@gmail.com>
Date: Wed, 5 Jan 2022 15:57:29 +0700
Subject: [PATCH] removed lab reducer

---
 .env.development                              |  1 +
 src/{redux/lab/api.ts => api/lab/index.ts}    |  2 +-
 src/components/main/Header/index.tsx          |  7 +-
 src/containers/lab/LabGrid/index.tsx          | 40 ++++-----
 src/hooks/flow/useFlow.ts                     |  7 +-
 src/hooks/lab/useGetLabNodes.ts               | 85 ++++++++++++++++++
 src/hooks/lab/useGetLabStats.ts               | 72 +++++++++++++++
 src/hooks/lab/useLab.ts                       | 36 ++------
 src/hooks/lab/useLabPagination.ts             | 44 ---------
 src/hooks/node/useCreateNode.ts               | 16 +---
 src/hooks/node/useOnNodeSeen.ts               | 10 +--
 src/hooks/node/useUpdateNode.ts               | 13 +--
 src/redux/auth/sagas.ts                       |  6 +-
 src/redux/lab/actions.ts                      | 40 ---------
 src/redux/lab/constants.ts                    | 14 ---
 src/redux/lab/handlers.ts                     | 38 --------
 src/redux/lab/index.ts                        | 24 -----
 src/redux/lab/sagas.ts                        | 90 -------------------
 src/redux/lab/selectors.ts                    | 10 ---
 src/redux/store.ts                            |  7 --
 src/store/index.ts                            |  2 +
 src/store/lab/LabStore.ts                     | 22 +++++
 src/store/lab/useLabStore.ts                  |  3 +
 .../lab/types.ts => types/lab/index.ts}       |  0
 src/utils/context/LabContextProvider.tsx      | 16 ++--
 src/utils/providers/LabProvider.tsx           |  6 +-
 26 files changed, 242 insertions(+), 369 deletions(-)
 rename src/{redux/lab/api.ts => api/lab/index.ts} (95%)
 create mode 100644 src/hooks/lab/useGetLabNodes.ts
 create mode 100644 src/hooks/lab/useGetLabStats.ts
 delete mode 100644 src/hooks/lab/useLabPagination.ts
 delete mode 100644 src/redux/lab/actions.ts
 delete mode 100644 src/redux/lab/constants.ts
 delete mode 100644 src/redux/lab/handlers.ts
 delete mode 100644 src/redux/lab/index.ts
 delete mode 100644 src/redux/lab/sagas.ts
 delete mode 100644 src/redux/lab/selectors.ts
 create mode 100644 src/store/lab/LabStore.ts
 create mode 100644 src/store/lab/useLabStore.ts
 rename src/{redux/lab/types.ts => types/lab/index.ts} (100%)

diff --git a/.env.development b/.env.development
index cf60c666..fbddcd27 100644
--- a/.env.development
+++ b/.env.development
@@ -1,3 +1,4 @@
 #REACT_APP_API_HOST=http://localhost:3334/
 REACT_APP_API_HOST=https://pig.staging.vault48.org/
+#REACT_APP_API_HOST=https://pig.vault48.org/
 REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
diff --git a/src/redux/lab/api.ts b/src/api/lab/index.ts
similarity index 95%
rename from src/redux/lab/api.ts
rename to src/api/lab/index.ts
index 87e6a7e8..6c9934d9 100644
--- a/src/redux/lab/api.ts
+++ b/src/api/lab/index.ts
@@ -5,7 +5,7 @@ import {
   GetLabNodesResult,
   GetLabStatsResult,
   GetLabUpdatesResult,
-} from '~/redux/lab/types';
+} from '~/types/lab';
 
 export const getLabNodes = ({ after }: GetLabNodesRequest) =>
   api
diff --git a/src/components/main/Header/index.tsx b/src/components/main/Header/index.tsx
index 489c2657..91a917c1 100644
--- a/src/components/main/Header/index.tsx
+++ b/src/components/main/Header/index.tsx
@@ -17,13 +17,12 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
 import { IState } from '~/redux/store';
 import isBefore from 'date-fns/isBefore';
 import { Authorized } from '~/components/containers/Authorized';
-import { useShallowSelect } from '~/hooks/data/useShallowSelect';
-import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
 import { Button } from '~/components/input/Button';
 import { useFlowStore } from '~/store/flow/useFlowStore';
 import { observer } from 'mobx-react';
 import { useShowModal } from '~/hooks/modal/useShowModal';
 import { Dialog } from '~/constants/modal';
+import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
 
 const mapStateToProps = (state: IState) => ({
   user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
@@ -49,9 +48,9 @@ const HeaderUnconnected: FC<IProps> = observer(
     authOpenProfile,
   }) => {
     const [is_scrolled, setIsScrolled] = useState(false);
-    const labUpdates = useShallowSelect(selectLabUpdatesNodes);
     const { updated: flowUpdates } = useFlowStore();
     const onLogin = useShowModal(Dialog.Login);
+    const labStats = useGetLabStats();
 
     const onScroll = useCallback(() => {
       const active = window.scrollY > 32;
@@ -76,7 +75,7 @@ const HeaderUnconnected: FC<IProps> = observer(
       [boris_commented_at, is_user, last_seen_boris]
     );
 
-    const hasLabUpdates = useMemo(() => labUpdates.length > 0, [labUpdates]);
+    const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [labStats.updates]);
     const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
 
     return (
diff --git a/src/containers/lab/LabGrid/index.tsx b/src/containers/lab/LabGrid/index.tsx
index 620aeef8..7feeb26f 100644
--- a/src/containers/lab/LabGrid/index.tsx
+++ b/src/containers/lab/LabGrid/index.tsx
@@ -1,11 +1,11 @@
-import React, { FC, useMemo } from 'react';
+import React, { FC } from 'react';
 import Masonry from 'react-masonry-css';
 import styles from './styles.module.scss';
 import { LabNode } from '~/components/lab/LabNode';
 import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
 import { values } from 'ramda';
-import { useLabPagination } from '~/hooks/lab/useLabPagination';
 import { useLabContext } from '~/utils/context/LabContextProvider';
+import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
 
 interface IProps {}
 
@@ -27,11 +27,7 @@ const LoadingNode = () => (
 );
 
 const LabGrid: FC<IProps> = () => {
-  const { isLoading, nodes, onLoadMore } = useLabContext();
-
-  const columns = useMemo(() => Array.from(document.querySelectorAll(`.${styles.column}`)), []);
-
-  useLabPagination(isLoading, columns, onLoadMore);
+  const { isLoading, nodes, hasMore, loadMore } = useLabContext();
 
   if (isLoading) {
     return (
@@ -54,20 +50,22 @@ const LabGrid: FC<IProps> = () => {
   }
 
   return (
-    <Masonry
-      className={styles.wrap}
-      breakpointCols={breakpointCols}
-      columnClassName={styles.column}
-    >
-      {nodes.map(node => (
-        <LabNode
-          node={node.node}
-          key={node.node.id}
-          lastSeen={node.last_seen}
-          commentCount={node.comment_count}
-        />
-      ))}
-    </Masonry>
+    <InfiniteScroll hasMore={hasMore} loadMore={loadMore}>
+      <Masonry
+        className={styles.wrap}
+        breakpointCols={breakpointCols}
+        columnClassName={styles.column}
+      >
+        {nodes.map(node => (
+          <LabNode
+            node={node.node}
+            key={node.node.id}
+            lastSeen={node.last_seen}
+            commentCount={node.comment_count}
+          />
+        ))}
+      </Masonry>
+    </InfiniteScroll>
   );
 };
 
diff --git a/src/hooks/flow/useFlow.ts b/src/hooks/flow/useFlow.ts
index 4d7c73f8..1514f0a4 100644
--- a/src/hooks/flow/useFlow.ts
+++ b/src/hooks/flow/useFlow.ts
@@ -1,22 +1,21 @@
-import { useShallowSelect } from '~/hooks/data/useShallowSelect';
 import { useFlowLayout } from '~/hooks/flow/useFlowLayout';
-import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
 import { useMemo } from 'react';
 import { useFlowLoader } from '~/hooks/flow/useFlowLoader';
 import { useFlowStore } from '~/store/flow/useFlowStore';
 import { useInfiniteLoader } from '~/hooks/dom/useInfiniteLoader';
 import { useFlowSetCellView } from '~/hooks/flow/useFlowSetCellView';
+import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
 
 export const useFlow = () => {
   const { loadMore, isSyncing } = useFlowLoader();
 
   const { nodes, heroes, recent, updated } = useFlowStore();
   const { isFluid, toggleLayout } = useFlowLayout();
-  const labUpdates = useShallowSelect(selectLabUpdatesNodes);
+  const lab = useGetLabStats();
 
   useInfiniteLoader(loadMore, isSyncing);
 
-  const updates = useMemo(() => [...updated, ...labUpdates].slice(0, 10), [updated, labUpdates]);
+  const updates = useMemo(() => [...updated, ...lab.updates].slice(0, 10), [lab.updates, updated]);
 
   const onChangeCellView = useFlowSetCellView();
   return { nodes, heroes, recent, updates, isFluid, toggleLayout, onChangeCellView };
diff --git a/src/hooks/lab/useGetLabNodes.ts b/src/hooks/lab/useGetLabNodes.ts
new file mode 100644
index 00000000..3cc44f46
--- /dev/null
+++ b/src/hooks/lab/useGetLabNodes.ts
@@ -0,0 +1,85 @@
+import useSWRInfinite from 'swr/infinite';
+import { KeyLoader } from 'swr';
+import { GetLabNodesRequest, ILabNode } from '~/types/lab';
+import { getLabNodes } from '~/api/lab';
+import { flatten, last, uniqBy } from 'ramda';
+import { useLabStore } from '~/store/lab/useLabStore';
+import { useCallback, useEffect } from 'react';
+import { INode } from '~/redux/types';
+import { useUser } from '~/hooks/user/userUser';
+
+const getKey: (isUser: boolean) => KeyLoader<ILabNode[]> = isUser => (index, prev) => {
+  if (!isUser) return null;
+  if (index > 0 && !prev?.length) return null;
+
+  const lastNode = last(prev || []);
+  if (!lastNode && index > 0) {
+    return null;
+  }
+
+  const props: GetLabNodesRequest = {
+    after: lastNode?.node.commented_at || lastNode?.node.created_at,
+  };
+
+  return JSON.stringify(props);
+};
+
+const parseKey = (key: string): GetLabNodesRequest => {
+  try {
+    return JSON.parse(key);
+  } catch (error) {
+    return {};
+  }
+};
+
+export const useGetLabNodes = () => {
+  const labStore = useLabStore();
+  const { is_user } = useUser();
+
+  const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
+    getKey(is_user),
+    async (key: string) => {
+      const result = await getLabNodes(parseKey(key));
+      return result.nodes;
+    },
+    {
+      fallbackData: [labStore.nodes],
+      onSuccess: data => labStore.setNodes(flatten(data)),
+    }
+  );
+
+  const nodes = uniqBy(n => n.node.id, flatten(data || []));
+  const hasMore = (data?.[size - 1]?.length || 0) >= 1;
+  const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
+
+  /** prepends list with a node */
+  const unshift = useCallback(
+    async (node: ILabNode) => {
+      await mutate([[node], ...(data || [])]);
+    },
+    [data, mutate]
+  );
+
+  /** updates node on cache */
+  const updateNode = useCallback(
+    async (nodeId: number, node: Partial<INode>) => {
+      await mutate(
+        data?.map(page =>
+          page.map(it => (it.node.id === nodeId ? { ...it, node: { ...it.node, node } } : it))
+        )
+      );
+    },
+    [data, mutate]
+  );
+
+  /** purge cache on exit */
+  useEffect(() => {
+    if (is_user) {
+      return;
+    }
+
+    labStore.setNodes([]);
+  }, [is_user, labStore]);
+
+  return { nodes, isLoading: !data && isValidating, hasMore, loadMore, unshift, updateNode };
+};
diff --git a/src/hooks/lab/useGetLabStats.ts b/src/hooks/lab/useGetLabStats.ts
new file mode 100644
index 00000000..f684955d
--- /dev/null
+++ b/src/hooks/lab/useGetLabStats.ts
@@ -0,0 +1,72 @@
+import useSWR from 'swr';
+import { API } from '~/constants/api';
+import { getLabStats, getLabUpdates } from '~/api/lab';
+import { useLabStore } from '~/store/lab/useLabStore';
+import { useCallback, useEffect } from 'react';
+import { useUser } from '~/hooks/user/userUser';
+
+const refreshInterval = 1000 * 60 * 5; // 5 minutes
+
+export const useGetLabStats = () => {
+  const lab = useLabStore();
+  const { is_user } = useUser();
+
+  const { data: stats, isValidating: isValidatingStats } = useSWR(
+    is_user ? API.LAB.STATS : null,
+    async () => getLabStats(),
+    {
+      fallbackData: {
+        heroes: lab.heroes,
+        tags: lab.tags,
+      },
+      onSuccess: data => {
+        lab.setHeroes(data.heroes);
+        lab.setTags(data.tags);
+      },
+      refreshInterval,
+    }
+  );
+
+  const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR(
+    is_user ? API.LAB.UPDATES : null,
+    async () => {
+      const result = await getLabUpdates();
+      return result.nodes;
+    },
+    {
+      fallbackData: lab.updates,
+      onSuccess: data => {
+        lab.setUpdates(data);
+      },
+      refreshInterval,
+    }
+  );
+
+  const heroes = stats?.heroes || [];
+  const tags = stats?.tags || [];
+  const updates = updatesData || [];
+
+  const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates);
+  const seenNode = useCallback(
+    async (nodeId: number) => {
+      await mutateUpdates(
+        updates.filter(it => it.id !== nodeId),
+        false
+      );
+    },
+    [mutateUpdates, updates]
+  );
+
+  /** purge cache on exit */
+  useEffect(() => {
+    if (is_user) {
+      return;
+    }
+
+    lab.setHeroes([]);
+    lab.setTags([]);
+    lab.setUpdates([]);
+  }, [is_user, lab]);
+
+  return { heroes, tags, updates, isLoading, seenNode };
+};
diff --git a/src/hooks/lab/useLab.ts b/src/hooks/lab/useLab.ts
index e3005ed9..31680ccc 100644
--- a/src/hooks/lab/useLab.ts
+++ b/src/hooks/lab/useLab.ts
@@ -1,35 +1,9 @@
-import { useShallowSelect } from '~/hooks/data/useShallowSelect';
-import {
-  selectLabList,
-  selectLabStatsHeroes,
-  selectLabStatsLoading,
-  selectLabStatsTags,
-  selectLabUpdatesNodes,
-} from '~/redux/lab/selectors';
-import { useDispatch } from 'react-redux';
-import { useCallback, useEffect } from 'react';
-import { labGetList, labGetMore, labGetStats } from '~/redux/lab/actions';
+import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
+import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
 
 export const useLab = () => {
-  const { is_loading: isLoading, nodes, count } = useShallowSelect(selectLabList);
-  const dispatch = useDispatch();
-  const tags = useShallowSelect(selectLabStatsTags);
-  const heroes = useShallowSelect(selectLabStatsHeroes);
-  const isLoadingStats = useShallowSelect(selectLabStatsLoading);
-  const updates = useShallowSelect(selectLabUpdatesNodes);
+  const { nodes, isLoading, loadMore, hasMore } = useGetLabNodes();
+  const { tags, heroes, updates, isLoading: isLoadingStats } = useGetLabStats();
 
-  useEffect(() => {
-    dispatch(labGetList());
-    dispatch(labGetStats());
-  }, [dispatch]);
-
-  const onLoadMore = useCallback(() => {
-    if (nodes.length >= count) {
-      return;
-    }
-
-    dispatch(labGetMore());
-  }, [nodes, count, dispatch]);
-
-  return { isLoading, nodes, count, onLoadMore, tags, heroes, isLoadingStats, updates };
+  return { isLoading, nodes, hasMore, loadMore, tags, heroes, isLoadingStats, updates };
 };
diff --git a/src/hooks/lab/useLabPagination.ts b/src/hooks/lab/useLabPagination.ts
deleted file mode 100644
index af71b7b0..00000000
--- a/src/hooks/lab/useLabPagination.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useCallback, useEffect, useMemo } from 'react';
-
-export const useLabPagination = (
-  isLoading: boolean,
-  columns: Element[],
-  onLoadMore: () => void
-) => {
-  const loadOnIntersection = useCallback<IntersectionObserverCallback>(
-    entries => {
-      const isVisible = entries.some(entry => entry.intersectionRatio > 0);
-
-      if (!isVisible) {
-        return;
-      }
-
-      onLoadMore();
-    },
-    [onLoadMore]
-  );
-
-  const observer = useMemo(
-    () =>
-      new IntersectionObserver(loadOnIntersection, {
-        threshold: [0],
-      }),
-    [loadOnIntersection]
-  );
-
-  useEffect(() => {
-    if (isLoading) {
-      return;
-    }
-
-    const lastItems = Array.from(columns)
-      .map(col => col.children.item(col.childNodes.length - 1))
-      .filter(el => el) as Element[];
-
-    lastItems.forEach(item => observer.observe(item));
-
-    return () => {
-      lastItems.forEach(item => observer.unobserve(item));
-    };
-  }, [observer, columns, isLoading]);
-};
diff --git a/src/hooks/node/useCreateNode.ts b/src/hooks/node/useCreateNode.ts
index b35456c5..f6d555a9 100644
--- a/src/hooks/node/useCreateNode.ts
+++ b/src/hooks/node/useCreateNode.ts
@@ -1,16 +1,12 @@
 import { useCallback } from 'react';
 import { INode } from '~/redux/types';
 import { apiPostNode } from '~/api/node';
-import { selectLabListNodes } from '~/redux/lab/selectors';
-import { labSetList } from '~/redux/lab/actions';
-import { useShallowSelect } from '~/hooks/data/useShallowSelect';
-import { useDispatch } from 'react-redux';
 import { useFlowStore } from '~/store/flow/useFlowStore';
+import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
 
 export const useCreateNode = () => {
-  const dispatch = useDispatch();
   const flow = useFlowStore();
-  const labNodes = useShallowSelect(selectLabListNodes);
+  const lab = useGetLabNodes();
 
   return useCallback(
     async (node: INode) => {
@@ -19,13 +15,9 @@ export const useCreateNode = () => {
       if (node.is_promoted) {
         flow.setNodes([result.node, ...flow.nodes]);
       } else {
-        const updatedNodes = [
-          { node: result.node, comment_count: 0, last_seen: node.created_at },
-          ...labNodes,
-        ];
-        dispatch(labSetList({ nodes: updatedNodes }));
+        await lab.unshift({ node: result.node, comment_count: 0, last_seen: node.created_at });
       }
     },
-    [flow, labNodes, dispatch]
+    [flow, lab]
   );
 };
diff --git a/src/hooks/node/useOnNodeSeen.ts b/src/hooks/node/useOnNodeSeen.ts
index e1508bf3..65f94b5c 100644
--- a/src/hooks/node/useOnNodeSeen.ts
+++ b/src/hooks/node/useOnNodeSeen.ts
@@ -1,13 +1,12 @@
 import { INode } from '~/redux/types';
-import { useDispatch } from 'react-redux';
-import { labSeenNode } from '~/redux/lab/actions';
 import { useEffect } from 'react';
 import { useFlowStore } from '~/store/flow/useFlowStore';
+import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
 
 // useOnNodeSeen updates node seen status across all needed places
 export const useOnNodeSeen = (node?: INode) => {
+  const labStats = useGetLabStats();
   const flow = useFlowStore();
-  const dispatch = useDispatch();
 
   useEffect(() => {
     if (!node?.id) {
@@ -18,7 +17,8 @@ export const useOnNodeSeen = (node?: INode) => {
     if (node.is_promoted) {
       flow.seenNode(node.id);
     } else {
-      dispatch(labSeenNode(node.id));
+      void labStats.seenNode(node.id);
     }
-  }, [dispatch, flow, node]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [node?.id]);
 };
diff --git a/src/hooks/node/useUpdateNode.ts b/src/hooks/node/useUpdateNode.ts
index cd702b19..907a0891 100644
--- a/src/hooks/node/useUpdateNode.ts
+++ b/src/hooks/node/useUpdateNode.ts
@@ -2,17 +2,15 @@ import { useLoadNode } from '~/hooks/node/useLoadNode';
 import { useCallback } from 'react';
 import { INode } from '~/redux/types';
 import { apiPostNode } from '~/api/node';
-import { selectLabListNodes } from '~/redux/lab/selectors';
-import { labSetList } from '~/redux/lab/actions';
-import { useShallowSelect } from '~/hooks/data/useShallowSelect';
 import { useDispatch } from 'react-redux';
 import { useFlowStore } from '~/store/flow/useFlowStore';
+import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
 
 export const useUpdateNode = (id: number) => {
   const dispatch = useDispatch();
   const { update } = useLoadNode(id);
   const flow = useFlowStore();
-  const labNodes = useShallowSelect(selectLabListNodes);
+  const lab = useGetLabNodes();
 
   return useCallback(
     async (node: INode) => {
@@ -27,12 +25,9 @@ export const useUpdateNode = (id: number) => {
       if (node.is_promoted) {
         flow.updateNode(result.node.id!, result.node);
       } else {
-        const updatedNodes = labNodes.map(item =>
-          item.node.id === result.node.id ? { ...item, node: result.node } : item
-        );
-        dispatch(labSetList({ nodes: updatedNodes }));
+        await lab.updateNode(result.node.id!, result.node);
       }
     },
-    [update, flow, labNodes, dispatch]
+    [update, flow, lab]
   );
 };
diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts
index df18f05b..4919339f 100644
--- a/src/redux/auth/sagas.ts
+++ b/src/redux/auth/sagas.ts
@@ -55,16 +55,15 @@ import { messagesSet } from '~/redux/messages/actions';
 import { SagaIterator } from 'redux-saga';
 import { isEmpty } from 'ramda';
 import { AxiosError } from 'axios';
-import { labGetUpdates } from '~/redux/lab/actions';
 import { getMOBXStore } from '~/store';
 import { Dialog } from '~/constants/modal';
 import { showErrorToast } from '~/utils/errors/showToast';
-import { showToastSuccess, showToastInfo } from '~/utils/toast';
+import { showToastInfo } from '~/utils/toast';
 import { getRandomPhrase } from '~/constants/phrases';
 
 const modalStore = getMOBXStore().modal;
 
-function* setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
+function setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
   localStorage.setItem('token', token);
 }
 
@@ -205,7 +204,6 @@ function* getUpdates() {
 function* startPollingSaga() {
   while (true) {
     yield call(getUpdates);
-    yield put(labGetUpdates());
     yield delay(60000);
   }
 }
diff --git a/src/redux/lab/actions.ts b/src/redux/lab/actions.ts
deleted file mode 100644
index 00dcc529..00000000
--- a/src/redux/lab/actions.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { LAB_ACTIONS } from '~/redux/lab/constants';
-import { ILabState } from '~/redux/lab/types';
-import { INode } from '~/redux/types';
-
-export const labGetList = (after?: string) => ({
-  type: LAB_ACTIONS.GET_LIST,
-  after,
-});
-
-export const labSetList = (list: Partial<ILabState['list']>) => ({
-  type: LAB_ACTIONS.SET_LIST,
-  list,
-});
-
-export const labGetStats = () => ({
-  type: LAB_ACTIONS.GET_STATS,
-});
-
-export const labSetStats = (stats: Partial<ILabState['stats']>) => ({
-  type: LAB_ACTIONS.SET_STATS,
-  stats,
-});
-
-export const labSetUpdates = (updates: Partial<ILabState['updates']>) => ({
-  type: LAB_ACTIONS.SET_UPDATES,
-  updates,
-});
-
-export const labGetUpdates = () => ({
-  type: LAB_ACTIONS.GET_UPDATES,
-});
-
-export const labSeenNode = (nodeId: INode['id']) => ({
-  type: LAB_ACTIONS.SEEN_NODE,
-  nodeId,
-});
-
-export const labGetMore = () => ({
-  type: LAB_ACTIONS.GET_MORE,
-});
diff --git a/src/redux/lab/constants.ts b/src/redux/lab/constants.ts
deleted file mode 100644
index bc2e150e..00000000
--- a/src/redux/lab/constants.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-const prefix = 'LAB.';
-
-export const LAB_ACTIONS = {
-  GET_LIST: `${prefix}GET_LIST`,
-  SET_LIST: `${prefix}SET_LIST`,
-
-  GET_STATS: `${prefix}GET_STATS`,
-  SET_STATS: `${prefix}SET_STATS`,
-
-  SET_UPDATES: `${prefix}SET_UPDATES`,
-  GET_UPDATES: `${prefix}GET_UPDATES`,
-  SEEN_NODE: `${prefix}SET_UPDATE_SEEN`,
-  GET_MORE: `${prefix}GET_MORE`,
-};
diff --git a/src/redux/lab/handlers.ts b/src/redux/lab/handlers.ts
deleted file mode 100644
index 15c0b923..00000000
--- a/src/redux/lab/handlers.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { LAB_ACTIONS } from '~/redux/lab/constants';
-import { labSetList, labSetStats, labSetUpdates } from '~/redux/lab/actions';
-import { ILabState } from '~/redux/lab/types';
-
-type LabHandler<T extends (...args: any) => any> = (
-  state: Readonly<ILabState>,
-  payload: ReturnType<T>
-) => Readonly<ILabState>;
-
-const setList: LabHandler<typeof labSetList> = (state, { list }) => ({
-  ...state,
-  list: {
-    ...state.list,
-    ...list,
-  },
-});
-
-const setStats: LabHandler<typeof labSetStats> = (state, { stats }) => ({
-  ...state,
-  stats: {
-    ...state.stats,
-    ...stats,
-  },
-});
-
-const setUpdates: LabHandler<typeof labSetUpdates> = (state, { updates }) => ({
-  ...state,
-  updates: {
-    ...state.updates,
-    ...updates,
-  },
-});
-
-export const LAB_HANDLERS = {
-  [LAB_ACTIONS.SET_LIST]: setList,
-  [LAB_ACTIONS.SET_STATS]: setStats,
-  [LAB_ACTIONS.SET_UPDATES]: setUpdates,
-};
diff --git a/src/redux/lab/index.ts b/src/redux/lab/index.ts
deleted file mode 100644
index ba277a55..00000000
--- a/src/redux/lab/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { createReducer } from '~/utils/reducer';
-import { LAB_HANDLERS } from '~/redux/lab/handlers';
-import { ILabState } from '~/redux/lab/types';
-
-const INITIAL_STATE: ILabState = {
-  list: {
-    is_loading: false,
-    nodes: [],
-    count: 0,
-    error: '',
-  },
-  stats: {
-    is_loading: false,
-    heroes: [],
-    tags: [],
-    error: undefined,
-  },
-  updates: {
-    nodes: [],
-    isLoading: false,
-  },
-};
-
-export default createReducer(INITIAL_STATE, LAB_HANDLERS);
diff --git a/src/redux/lab/sagas.ts b/src/redux/lab/sagas.ts
deleted file mode 100644
index fc01d490..00000000
--- a/src/redux/lab/sagas.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { call, put, select, takeLeading } from 'redux-saga/effects';
-import {
-  labGetList,
-  labSeenNode,
-  labSetList,
-  labSetStats,
-  labSetUpdates,
-} from '~/redux/lab/actions';
-import { LAB_ACTIONS } from '~/redux/lab/constants';
-import { Unwrap } from '~/redux/types';
-import { getLabNodes, getLabStats, getLabUpdates } from '~/redux/lab/api';
-import { selectLabList, selectLabUpdatesNodes } from '~/redux/lab/selectors';
-
-function* getList({ after = '' }: ReturnType<typeof labGetList>) {
-  try {
-    yield put(labSetList({ is_loading: true }));
-    const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
-    yield put(labSetList({ nodes, count }));
-  } catch (error) {
-    yield put(labSetList({ error: error.message }));
-  } finally {
-    yield put(labSetList({ is_loading: false }));
-  }
-}
-
-function* getStats() {
-  try {
-    yield put(labSetStats({ is_loading: true }));
-    const { heroes, tags }: Unwrap<typeof getLabStats> = yield call(getLabStats);
-    yield put(labSetStats({ heroes, tags }));
-  } catch (error) {
-    yield put(labSetStats({ error: error.message }));
-  } finally {
-    yield put(labSetStats({ is_loading: false }));
-  }
-}
-
-function* getUpdates() {
-  try {
-    yield put(labSetUpdates({ isLoading: true }));
-    const { nodes }: Unwrap<typeof getLabUpdates> = yield call(getLabUpdates);
-    yield put(labSetUpdates({ nodes }));
-  } catch (error) {
-    console.log(error.message);
-  } finally {
-    yield put(labSetUpdates({ isLoading: false }));
-  }
-}
-
-function* seenNode({ nodeId }: ReturnType<typeof labSeenNode>) {
-  const nodes: ReturnType<typeof selectLabUpdatesNodes> = yield select(selectLabUpdatesNodes);
-  const newNodes = nodes.filter(node => node.id != nodeId);
-  yield put(labSetUpdates({ nodes: newNodes }));
-}
-
-function* getMore() {
-  try {
-    yield put(labSetList({ is_loading: true }));
-
-    const list: ReturnType<typeof selectLabList> = yield select(selectLabList);
-    if (list.nodes.length === list.count) {
-      return;
-    }
-
-    const last = list.nodes[list.nodes.length - 1];
-
-    if (!last) {
-      return;
-    }
-    const after = last.node.commented_at || last.node.created_at;
-    const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
-    const newNodes = [...list.nodes, ...nodes];
-
-    yield put(labSetList({ nodes: newNodes, count }));
-  } catch (error) {
-    yield put(labSetList({ error: error.message }));
-  } finally {
-    yield put(labSetList({ is_loading: false }));
-  }
-}
-
-export default function* labSaga() {
-  yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
-  yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
-
-  yield takeLeading(LAB_ACTIONS.GET_UPDATES, getUpdates);
-  yield takeLeading(LAB_ACTIONS.SEEN_NODE, seenNode);
-
-  yield takeLeading(LAB_ACTIONS.GET_MORE, getMore);
-}
diff --git a/src/redux/lab/selectors.ts b/src/redux/lab/selectors.ts
deleted file mode 100644
index 6b9e52ca..00000000
--- a/src/redux/lab/selectors.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { IState } from '~/redux/store';
-
-export const selectLab = (state: IState) => state.lab;
-export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
-export const selectLabList = (state: IState) => state.lab.list;
-export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes;
-export const selectLabStatsTags = (state: IState) => state.lab.stats.tags;
-export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading;
-export const selectLabUpdates = (state: IState) => state.lab.updates;
-export const selectLabUpdatesNodes = (state: IState) => state.lab.updates.nodes;
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 6ffd230d..c4402e9d 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -11,10 +11,6 @@ import auth from '~/redux/auth';
 import authSaga from '~/redux/auth/sagas';
 import { IAuthState } from '~/redux/auth/types';
 
-import lab from '~/redux/lab';
-import labSaga from '~/redux/lab/sagas';
-import { ILabState } from '~/redux/lab/types';
-
 import uploads, { IUploadState } from '~/redux/uploads/reducer';
 import uploadSaga from '~/redux/uploads/sagas';
 
@@ -48,7 +44,6 @@ export interface IState {
   uploads: IUploadState;
   player: IPlayerState;
   messages: IMessagesState;
-  lab: ILabState;
 }
 
 export const sagaMiddleware = createSagaMiddleware();
@@ -68,7 +63,6 @@ export const store = createStore(
     uploads,
     player: persistReducer(playerPersistConfig, player),
     messages,
-    lab: lab,
   }),
   composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
 );
@@ -81,7 +75,6 @@ export function configureStore(): {
   sagaMiddleware.run(uploadSaga);
   sagaMiddleware.run(playerSaga);
   sagaMiddleware.run(messagesSaga);
-  sagaMiddleware.run(labSaga);
 
   window.addEventListener('message', message => {
     if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
diff --git a/src/store/index.ts b/src/store/index.ts
index 85009ca3..78924d92 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -2,11 +2,13 @@ import { makeAutoObservable } from 'mobx';
 import { FlowStore } from '~/store/flow/FlowStore';
 import { ModalStore } from '~/store/modal/ModalStore';
 import { PhotoSwipeStore } from '~/store/photoSwipe/PhotoSwipeStore';
+import { LabStore } from '~/store/lab/LabStore';
 
 export class Store {
   flow = new FlowStore();
   modal = new ModalStore();
   photoSwipe = new PhotoSwipeStore();
+  lab = new LabStore();
 
   constructor() {
     makeAutoObservable(this);
diff --git a/src/store/lab/LabStore.ts b/src/store/lab/LabStore.ts
new file mode 100644
index 00000000..498c3dce
--- /dev/null
+++ b/src/store/lab/LabStore.ts
@@ -0,0 +1,22 @@
+import { makeAutoObservable } from 'mobx';
+import { IFlowNode, ITag } from '~/redux/types';
+import { ILabNode } from '~/types/lab';
+
+export class LabStore {
+  // actual nodes
+  nodes: ILabNode[] = [];
+
+  // stats
+  heroes: IFlowNode[] = [];
+  tags: ITag[] = [];
+  updates: IFlowNode[] = [];
+
+  constructor() {
+    makeAutoObservable(this);
+  }
+
+  setNodes = (nodes: ILabNode[]) => (this.nodes = nodes);
+  setHeroes = (heroes: IFlowNode[]) => (this.heroes = heroes);
+  setTags = (tags: ITag[]) => (this.tags = tags);
+  setUpdates = (updates: IFlowNode[]) => (this.updates = updates);
+}
diff --git a/src/store/lab/useLabStore.ts b/src/store/lab/useLabStore.ts
new file mode 100644
index 00000000..a7698cdf
--- /dev/null
+++ b/src/store/lab/useLabStore.ts
@@ -0,0 +1,3 @@
+import { useStore } from '~/utils/context/StoreContextProvider';
+
+export const useLabStore = () => useStore().lab;
diff --git a/src/redux/lab/types.ts b/src/types/lab/index.ts
similarity index 100%
rename from src/redux/lab/types.ts
rename to src/types/lab/index.ts
diff --git a/src/utils/context/LabContextProvider.tsx b/src/utils/context/LabContextProvider.tsx
index 0c347f5d..9f1d31a3 100644
--- a/src/utils/context/LabContextProvider.tsx
+++ b/src/utils/context/LabContextProvider.tsx
@@ -1,24 +1,24 @@
-import { ILabNode } from '~/redux/lab/types';
+import { ILabNode } from '~/types/lab';
 import React, { createContext, FC, useContext } from 'react';
-import { INode, ITag } from '~/redux/types';
+import { IFlowNode, ITag } from '~/redux/types';
 
 export interface LabContextProps {
   isLoading: boolean;
   nodes: ILabNode[];
-  count: number;
-  onLoadMore: () => void;
+  hasMore: boolean;
+  loadMore: () => void;
 
   tags: ITag[];
-  heroes: Partial<INode>[];
+  heroes: IFlowNode[];
   isLoadingStats: boolean;
-  updates: Partial<INode>[];
+  updates: IFlowNode[];
 }
 
 const defaultValues: LabContextProps = {
   isLoading: false,
   nodes: [],
-  count: 0,
-  onLoadMore: () => {},
+  hasMore: false,
+  loadMore: () => {},
   tags: [],
   heroes: [],
   isLoadingStats: false,
diff --git a/src/utils/providers/LabProvider.tsx b/src/utils/providers/LabProvider.tsx
index 087f19aa..8531893d 100644
--- a/src/utils/providers/LabProvider.tsx
+++ b/src/utils/providers/LabProvider.tsx
@@ -5,14 +5,14 @@ import { useLab } from '~/hooks/lab/useLab';
 interface LabProviderProps {}
 
 const LabProvider: FC<LabProviderProps> = ({ children }) => {
-  const { isLoading, nodes, count, onLoadMore, tags, heroes, isLoadingStats, updates } = useLab();
+  const { isLoading, nodes, loadMore, hasMore, tags, heroes, isLoadingStats, updates } = useLab();
 
   return (
     <LabContextProvider
       isLoading={isLoading && !nodes.length}
       nodes={nodes}
-      count={count}
-      onLoadMore={onLoadMore}
+      hasMore={hasMore}
+      loadMore={loadMore}
       tags={tags}
       heroes={heroes}
       isLoadingStats={isLoadingStats}