Skip to content

Commit

Permalink
refactor: use context API to share channel videos data
Browse files Browse the repository at this point in the history
  • Loading branch information
AXeL-dev committed Sep 24, 2022
1 parent 39f663b commit 8ece712
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 130 deletions.
29 changes: 16 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,28 @@ import CssBaseline from '@mui/material/CssBaseline';
import useTheme from 'ui/theme';
import { useAppSelector } from 'store';
import { selectMode } from 'store/selectors/settings';
import { ChannelVideosProvider } from 'providers';

function App() {
const mode = useAppSelector(selectMode);
const theme = useTheme(mode);

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/channels" component={Channels} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/about" component={About} />
<Route path="/background" component={Background} />
<Redirect to="/" />
</Switch>
</Router>
</ThemeProvider>
<ChannelVideosProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/channels" component={Channels} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/about" component={About} />
<Route path="/background" component={Background} />
<Redirect to="/" />
</Switch>
</Router>
</ThemeProvider>
</ChannelVideosProvider>
);
}

Expand Down
17 changes: 10 additions & 7 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,18 @@ export function formatByteSize(bytes: number) {
* @param callback
* @param timeFrame
*/
export function throttle(callback: Function, timeFrame: number) {
export function throttle<F extends Function>(
callback: F,
timeFrame: number,
): F {
let lastTime = 0;
return (...args: any) => {
return ((...args: any) => {
let now = new Date().getTime();
if (now - lastTime >= timeFrame) {
callback(...args);
lastTime = now;
}
};
}) as any;
}

// -------------------------------------------------------------------
Expand All @@ -310,11 +313,11 @@ export function throttle(callback: Function, timeFrame: number) {
* @param wait
* @param immediate
*/
export function debounce(
callback: Function,
export function debounce<F extends Function>(
callback: F,
wait: number,
immediate?: boolean,
) {
): F {
let timeout: any = null;
return function (this: any, ...args: any) {
const context = this;
Expand All @@ -324,7 +327,7 @@ export function debounce(
if (!immediate) callback.apply(context, args);
}, wait);
if (immediate && !timeout) callback.apply(context, args);
};
} as any;
}

// -------------------------------------------------------------------
Expand Down
146 changes: 146 additions & 0 deletions src/providers/ChannelVideosProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { debounce } from 'helpers/utils';
import {
createContext,
FC,
memo,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import { GetChannelVideosResponse } from 'store/services/youtube';
import { Channel, Video, HomeView } from 'types';

export interface ChannelData extends GetChannelVideosResponse {
channel: Channel;
}

export interface ChannelVideosCount {
displayed: number;
total: number;
}

type ChannelVideosContextType = {
videosCount: { [key: string]: ChannelVideosCount };
setChannelData: (view: HomeView, data: ChannelData) => void;
clearChannelsData: (view: HomeView) => void;
getLatestChannelVideo: (
view: HomeView,
channelId: string,
) => Video | undefined;
};

const ALL_VIEWS = [HomeView.All, HomeView.Recent, HomeView.WatchLater];

const INITIAL_COUNT = ALL_VIEWS.reduce(
(acc, view) => ({
...acc,
[view]: {
displayed: 0,
total: 0,
},
}),
{},
);

const ChannelVideosContext = createContext<
ChannelVideosContextType | undefined
>(undefined);

export const ChannelVideosProvider: FC = memo(({ children }) => {
const [videosCount, setVideosCount] =
useState<ChannelVideosContextType['videosCount']>(INITIAL_COUNT);
const channelsMap = useRef<{ [key: string]: Map<string, ChannelData> }>(
ALL_VIEWS.reduce((acc, view) => ({ ...acc, [view]: new Map() }), {}),
);

const updateCount = useCallback(
debounce((view: HomeView, count: ChannelVideosCount) => {
setVideosCount((state) => ({
...state,
[view]: count,
}));
}, 200),
[],
);

const setChannelData = (view: HomeView, data: ChannelData) => {
// save channel data per view
channelsMap.current[view].set(data.channel.id, data);
// update videos count per view
const channelsData = Array.from(channelsMap.current[view].values());
const count = channelsData.reduce(
(acc, cur) => ({
displayed: acc.displayed + (cur.items?.length || 0),
total: acc.total + (cur.total || 0),
}),
{ displayed: 0, total: 0 },
);
updateCount(view, count);
};

const clearChannelsData = (view: HomeView) => {
channelsMap.current[view].clear();
setVideosCount((state) => ({
...state,
[view]: {
displayed: 0,
total: 0,
},
}));
};

const getLatestChannelVideo = (view: HomeView, channelId: string) => {
return channelsMap.current[view].get(channelId)?.items[0];
};

const value = useMemo(
() => ({
videosCount,
setChannelData,
clearChannelsData,
getLatestChannelVideo,
}),
[videosCount],
);

return (
<ChannelVideosContext.Provider value={value}>
{children}
</ChannelVideosContext.Provider>
);
});

type ChannelVideosHookType = {
videosCount: ChannelVideosCount;
setChannelData: (data: ChannelData) => void;
clearChannelsData: () => void;
getLatestChannelVideo: (channelId: string) => Video | undefined;
};

export function useChannelVideos(view: HomeView): ChannelVideosHookType {
const context = useContext(ChannelVideosContext);

if (context === undefined) {
throw new Error(
'useChannelVideos must be used within a ChannelVideosContext',
);
}

const {
videosCount,
setChannelData,
clearChannelsData,
getLatestChannelVideo,
} = context;

return {
videosCount: videosCount[view],
setChannelData: (data) => setChannelData(view, data),
clearChannelsData: () => clearChannelsData(view),
getLatestChannelVideo: (channelId) =>
getLatestChannelVideo(view, channelId),
};
}
1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ChannelVideosProvider';
2 changes: 1 addition & 1 deletion src/store/selectors/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const selectChannelVideos = (channel: Channel) =>
videos.filter(({ channelId }) => channel.id === channelId),
);

export const selectRecentChannelVideos = (channel: Channel) =>
export const selectClassifiedRecentChannelVideos = (channel: Channel) =>
createSelector(
selectChannelVideos(channel),
selectViewFilters(HomeView.Recent),
Expand Down
17 changes: 11 additions & 6 deletions src/ui/components/pages/Home/ChannelRenderer/DefaultRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@ import {
import ChannelRenderer from './ChannelRenderer';
import config from './ChannelVideos/config';
import { useGrid } from 'hooks';
import { useChannelVideos } from 'providers';

export interface DefaultRendererProps {
channel: Channel;
view: HomeView;
channel: Channel;
publishedAfter?: string;
persistVideosOptions?: PersistVideosOptions;
filter?: (video: Video) => boolean;
onError?: (error: any) => void;
onChange?: (data: any) => void;
onVideoPlay: (video: Video) => void;
}

// should be instanciated outside the component to avoid multi-rendering
const defaultFilter = () => true;

function DefaultRenderer(props: DefaultRendererProps) {
const {
view,
channel,
publishedAfter,
persistVideosOptions,
filter = () => true,
filter = defaultFilter,
onError,
onChange,
...rest
} = props;
const [page, setPage] = useState(1);
const { itemsPerRow = 0 } = useGrid(config.gridColumns);
const { setChannelData } = useChannelVideos(view);
const maxResults = itemsPerRow * page;
const { data, error, isLoading, isFetching } = useGetChannelVideosQuery(
{
Expand All @@ -57,14 +61,15 @@ function DefaultRenderer(props: DefaultRendererProps) {
}, [error, onError]);

useEffect(() => {
if (!isFetching && data && onChange) {
onChange({ channel, items: videos, total });
if (!isFetching && data) {
setChannelData({ channel, items: videos, total });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFetching, data, filter]);

return (
<ChannelRenderer
view={view}
channel={channel}
videos={videos}
total={total}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useAppSelector } from 'store';
import { selectSettings } from 'store/selectors/settings';
import { getDateBefore } from 'helpers/utils';
import DefaultRenderer, { DefaultRendererProps } from './DefaultRenderer';
import { selectRecentChannelVideos } from 'store/selectors/videos';
import { selectClassifiedRecentChannelVideos } from 'store/selectors/videos';
import { Video } from 'types';

export interface RecentViewRendererProps
Expand All @@ -12,7 +12,10 @@ export interface RecentViewRendererProps
function RecentViewRenderer(props: RecentViewRendererProps) {
const { channel } = props;
const settings = useAppSelector(selectSettings);
const videos = useAppSelector(selectRecentChannelVideos(channel));
const videos = useAppSelector(
selectClassifiedRecentChannelVideos(channel),
(left, right) => JSON.stringify(left) === JSON.stringify(right),
);
const filterCallback = useCallback(
(video: Video) =>
settings.recentViewFilters.others
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { selectWatchLaterVideos } from 'store/selectors/videos';
import ChannelRenderer from './ChannelRenderer';
import config from './ChannelVideos/config';
import { useGrid } from 'hooks';
import { useChannelVideos } from 'providers';

export interface WatchLaterViewRendererProps {
channel: Channel;
Expand All @@ -17,6 +18,7 @@ function WatchLaterViewRenderer(props: WatchLaterViewRendererProps) {
const { channel, onError, onVideoPlay } = props;
const [page, setPage] = useState(1);
const { itemsPerRow = 0 } = useGrid(config.gridColumns);
const { setChannelData } = useChannelVideos(HomeView.WatchLater);
const watchLaterVideos = useAppSelector(selectWatchLaterVideos(channel));
const ids = watchLaterVideos.map(({ id }) => id);
const total = ids.length;
Expand All @@ -42,6 +44,13 @@ function WatchLaterViewRenderer(props: WatchLaterViewRendererProps) {
}
}, [error, onError]);

useEffect(() => {
if (!isFetching && data) {
setChannelData({ channel, items: videos, total });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFetching, data]);

return (
<ChannelRenderer
view={HomeView.WatchLater}
Expand Down
4 changes: 1 addition & 3 deletions src/ui/components/pages/Home/ChannelsWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ interface ChannelsWrapperProps {
view: HomeView;
channels: Channel[];
onError?: (error: any) => void;
onChange?: (data: any) => void;
onVideoPlay: (video: Video) => void;
}

function ChannelsWrapper(props: ChannelsWrapperProps) {
const { view, channels, onError, onChange, onVideoPlay } = props;
const { view, channels, onError, onVideoPlay } = props;
const ChannelRenderer = useMemo(() => {
switch (view) {
case HomeView.WatchLater:
Expand Down Expand Up @@ -45,7 +44,6 @@ function ChannelsWrapper(props: ChannelsWrapperProps) {
view={view}
channel={channel}
onError={onError}
onChange={onChange}
onVideoPlay={onVideoPlay}
/>
))}
Expand Down
Loading

0 comments on commit 8ece712

Please sign in to comment.