-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add usePager
hook
#798
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a7f678c
feat: add PagerViewContext and usePagerView hook
gronxb 120d9b5
feat: usePagerView example
gronxb d255bb4
refactor: error when calling outside of pager view
gronxb 95f2d1d
refactor: optimized rendering using useSyncExternalStore
gronxb e28a6db
refactor: example render test
gronxb 82c7a39
refactor: rename example file
gronxb c1c4446
docs: usePager Hook Usage section
gronxb ed5a39b
refactor: unused PagerViewStare and rename filename
gronxb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import PagerView, { usePager } from 'react-native-pager-view'; | ||
import React from 'react'; | ||
import { View, StyleSheet, Button } from 'react-native'; | ||
import { Text } from 'react-native'; | ||
|
||
const NonHookComponent = () => { | ||
console.log('rerender <NonHookComponent />'); | ||
|
||
return ( | ||
<View style={styles.content}> | ||
<Text>HasNotHook</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
const HookComponent = ({ index }: { index: number }) => { | ||
const { | ||
page, | ||
hasNextPage, | ||
hasPreviousPage, | ||
setPage, | ||
setPageWithoutAnimation, | ||
setScrollEnabled, | ||
} = usePager(); | ||
|
||
console.log(`rerender <HookComponent index={${index}} />`); | ||
|
||
return ( | ||
<View style={styles.content}> | ||
<Text>Current Page: {page}</Text> | ||
<Text>hasNextPage: {String(hasNextPage)}</Text> | ||
<Text>hasPreviousPage: {String(hasPreviousPage)}</Text> | ||
<Button | ||
title="next page" | ||
onPress={() => { | ||
if (hasNextPage) { | ||
setPage(page + 1); | ||
} | ||
}} | ||
/> | ||
<Button | ||
title="prev page" | ||
onPress={() => { | ||
if (hasPreviousPage) { | ||
setPage(page - 1); | ||
} | ||
}} | ||
/> | ||
|
||
<Button | ||
title="next page without animation" | ||
onPress={() => { | ||
setPageWithoutAnimation(page + 1); | ||
}} | ||
/> | ||
|
||
<Button | ||
title="setScrollEnabled to true" | ||
onPress={() => { | ||
setScrollEnabled(true); | ||
}} | ||
/> | ||
|
||
<Button | ||
title="setScrollEnabled to false" | ||
onPress={() => { | ||
setScrollEnabled(false); | ||
}} | ||
/> | ||
</View> | ||
); | ||
}; | ||
|
||
export const UsePagerExample = (): JSX.Element => { | ||
return ( | ||
<PagerView testID="pager-view" style={styles.flex}> | ||
<HookComponent index={1} /> | ||
<HookComponent index={2} /> | ||
<HookComponent index={3} /> | ||
<NonHookComponent /> | ||
</PagerView> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
flex: { | ||
flex: 1, | ||
}, | ||
content: { | ||
flex: 1, | ||
marginVertical: 10, | ||
}, | ||
separator: { | ||
margin: 16, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { useContext, useSyncExternalStore } from 'react'; | ||
|
||
export type PagerViewContextValue = { | ||
store: PagerStore | null; | ||
setPage: (selectedPage: number) => void; | ||
setPageWithoutAnimation: (selectedPage: number) => void; | ||
setScrollEnabled: (scrollEnabled: boolean) => void; | ||
}; | ||
|
||
export type PagerState = { | ||
page: number; | ||
hasNextPage: boolean; | ||
hasPreviousPage: boolean; | ||
}; | ||
|
||
export type PagerStore = { | ||
getState: () => PagerState; | ||
setState: (state: PagerState) => void; | ||
subscribe: (listener: () => void) => () => void; | ||
}; | ||
|
||
export const PagerViewContext = | ||
React.createContext<PagerViewContextValue | null>(null); | ||
|
||
export const createPagerStore = (initialPage: number) => { | ||
let state: PagerState = { | ||
page: initialPage, | ||
hasNextPage: false, | ||
hasPreviousPage: false, | ||
}; | ||
|
||
const getState = () => { | ||
return state; | ||
}; | ||
|
||
const listeners = new Set<() => void>(); | ||
|
||
const emitChange = () => { | ||
for (const listener of listeners) { | ||
listener(); | ||
} | ||
}; | ||
|
||
const setState = (newState: PagerState) => { | ||
state = newState; | ||
emitChange(); | ||
}; | ||
|
||
const subscribe = (listener: () => void) => { | ||
listeners.add(listener); | ||
return () => listeners.delete(listener); | ||
}; | ||
|
||
return { getState, setState, subscribe }; | ||
}; | ||
|
||
export const usePager = () => { | ||
if (Number(React.version.split('.')[0]) < 18) { | ||
throw new Error('usePager requires React 18 or later.'); | ||
} | ||
const value = useContext(PagerViewContext); | ||
|
||
if (!value || !value.store) { | ||
throw new Error('usePager must be used within a <PagerView /> component'); | ||
} | ||
|
||
const { store, ...methods } = value; | ||
|
||
const state = useSyncExternalStore(store.subscribe, store.getState); | ||
return { ...methods, ...state }; | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[high]:
React Context is a feature of React that allows you to pass data through the component tree without having to pass props down manually at every level. However, it is not a state management tool, and it has some performance implications that you should be aware of.
One of the main performance issues with React Context is that when a context value changes, all components that use the
useContext
hook or theConsumer
component will re-render, regardless of whether they use the changed value or not. This can cause unnecessary re-rendering of components that are not affected by the context change, and thus affect the performance of your app.[suggestion]
Would you mind creating a separate file called
PagerViewContext
and moving the Context there? I don’t want to affect other projects. Moreover, this is breaking changes due to behavior changes. After that, could you update the readme file with a section about theusePager
hook?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your suggestion
When separating the React Context Provider into a separate file, there are issues with inserting the value. The fundamental problem is that re-rendering occurs even when the
usePager
hook is not used. (Originally, it was namedusePagerView
, but I changed it following your suggestion.)I thought it would be good to solve the re-rendering issue without changing the way it is used. Therefore, I chose to use
useSyncExternalStore
to configure the store and then inject it into the context, changing the value of the store.Since only the store’s value changes, rendering does not occur in components that do not use
usePager
.However, one concern is that
useSyncExternalStore
is a React 18 feature, which is not compatible with earlier versions. But since the example also uses version 18, and it has been a while since version 18 was released, I believe it would be fine if we insert a warning message.Lastly, one issue I have been contemplating is that even with the current usage, re-rendering occurs when there is a change in the page, even if one only wants to use setPage. For example,
const {setPage} = usePager();
In
usePager()
, we could export only methods likesetPage
,setPageWithoutAnimation
,setScrollEnabled
, and construct a selector likeusePagerState((state) => state.page);
. This would perfectly optimize rendering only for the parts subscribing to the state, but it might make the usage somewhat more cumbersome.If you agree with the approach I am taking, I would appreciate your opinion on this matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
<NonHookComponent />
inside the<PagerView />
does not use theusePager
hook. (You can also see the code at the bottom of the video.) Additionally, I have set it up to output a console.log statement every time it renders.The full code for this example can be found in the example code.
as-is.mov
useSyncExternalStore
)to-be.mov