I'm using a React Native Viewpager to take in user entry, and move to the next page on button press. Important to note that moving to the next page happens on button press, and not by normal scrolling, which is disabled.
The best way I could think to handle this was to have a state on the ViewPager, which would propagate into the child Entries.
ViewPager.tsx:
export default function ViewPager({ route, navigation }) {
const ref: React.RefObject<ViewPager> = React.createRef();
const [currentPage, setCurrentPage] = useState(0);
let setEntryPage = (page: number) => {
ref.current?.setPage(page);
setCurrentPage(page);
}
return (
<View style={{flex: 1}}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={ref}
scrollEnabled={false}
>
{
GlobalStuff.map((entry, index) => {
return (
<Entry
key={index}
index={index}
pagerFocusIndex={currentPage}
pagerLength={quizDeck?.litems.length!}
setEntryPage={setEntryPage}
/>
)
})
}
</ViewPager>
</View>
);
};
Entry.tsx:
export function Entry(props: EntryProps) {
const inputRef: React.RefObject<Input> = React.createRef();
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
return (
<View>
<Input
// ...
ref={inputRef}
/>
<IconButton
icon="arrow-right-thick"
color={colorTheme.green}
onPress={() => {
props.index !== props.pagerLength - 1 ?
props.setEntryPage(props.index + 1) :
props.navigation!.reset({ index: 0, routes: [{ name: recapScreenName as any }] });
}}
/>
// ...
Unfortunately, inputRef appears to be null, and there is probably a better way of achieving what I'm trying to achieve anyway.
Anything in your render loop will be called every time the component renders.
// This is called on every render
const inputRef: React.RefObject<Input> = React.createRef();
// So is this, it's always null
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
Put side effects in effects.
// Untested
const inputRef = useRef();
useEffect(() => {
if (props.pagerFocusIndex === props.index) {
inputRef.current?.focus();
}
}, [inputRef.current, props.pagerFocusIndex, props.index]);
Related
I'm trying to make a chronometer component who hides himself after a delay. It works but I have a warning when the Chronometer disappear and I don't know how to deal with it.
Warning: Cannot update a component (WorkoutScreen) while rendering a different component (Chronometer). To locate the bad setState() call inside Chronometer
WorkoutScreen.tsx
const WorkoutScreen = ({
navigation,
route,
}: RootStackScreenProps<"Workout">) => {
const [inRest, setInRest] = useState(false)
const [restTime, setRestTime] = useState(5)
//I pass it to child
const handleEndRestTime = () => {
setInRest(false)
}
//
return (
<Layout style={styles.container}>
<Button
onPress={() => {
setInRest(!inRest)
}}
>
Trigger chronometer
</Button>
{inRest && (
<Chronometer onEnd={handleEndRestTime} seconds={restTime}></Chronometer>
)}
</Layout>
)
}
Chronometer.tsx
const Chronometer = ({ seconds, onEnd }: Props) => {
const [timer, setTimer] = useState<number>(seconds)
const [pause, setPause] = useState(false)
const [running, setRunning] = useState(true)
useEffect(() => {
let interval: NodeJS.Timer
if (pause === true || running === false) {
;() => clearInterval(interval)
} else {
interval = setInterval(() => {
setTimer((timer) => timer - 1)
}, 1000)
}
return () => {
clearInterval(interval)
}
}, [pause, running])
if (timer === 0 && running === true) {
setRunning(false)
//From parent
onEnd()
//
}
return (
<View style={styles.container}>
<View style={styles.chronometer}>
<View style={styles.controls}>
<Text>{formatHhMmSs(timer)}</Text>
</View>
<Button
onPress={() => {
setPause(!pause)
}}
>
Pause
</Button>
</View>
</View>
)
}
When I remove the "{inRest && " the warning disappear.
In the future I want that the User can retrigger Chronometer as he want
Thanks in advance !
Warning on my emulator (1)
Warning on my emulator (2)
Warning on my emulator (3)
Warning on my terminal
There are two state updates that happen simultaneously and conflict with React rendering UI reconciliation
setRunning(false) inside the Chronometer component will rerender this component when the timer ends.
setInRest(false) inside WorkoutScreen component will also rerender when the timer ends.
Both those rerenders happen at the same timer and WorkoutScreen rerender is triggered by the child component.
The solution is to avoid triggering state change inside the parent component caused by the child component.
const WorkoutScreen = ({
navigation,
route,
}: RootStackScreenProps<"Workout">) => {
const [restTime, setRestTime] = useState(5);
//I pass it to child
const handleEndRestTime = () => {
// Handle logic when workout time end
};
//
return (
<Layout style={styles.container}>
<Chronometer onEnd={handleEndRestTime} seconds={restTime}></Chronometer>
</Layout>
);
};
const Chronometer = ({ seconds, onEnd }: Props) => {
const [timer, setTimer] = useState < number > seconds;
const [pause, setPause] = useState(false);
const [running, setRunning] = useState(true);
const [inRest, setInRest] = useState(false);
useEffect(() => {
let interval: NodeJS.Timer;
if (pause === true || running === false) {
() => clearInterval(interval);
} else {
interval = setInterval(() => {
setTimer((timer) => timer - 1);
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [pause, running]);
if (timer === 0 && running === true) {
setRunning(false);
setInRest(false);
//From parent
onEnd();
//
}
return (
<View style={styles.container}>
<Button
onPress={() => {
setInRest(!inRest);
}}
>
Trigger chronometer
</Button>
{inRest && (
<View style={styles.chronometer}>
{/* Show Timer counter only when is running */}
{running && (
<View style={styles.controls}>
<Text>{formatHhMmSs(timer)}</Text>
</View>
)}
<Button
onPress={() => {
setPause(!pause);
}}
>
{running ? "Pause" : "Start"}
</Button>
</View>
)}
</View>
);
};
I have a RadioGroup component that contains multiple RadioButton components. Here's the code for the RadioGroup component:
const RadioGroup = ({radioGroupData}) => {
const [radioGroupRefreshData, setRadioGroupRefreshData] = useState(radioGroupData);
const handleClick = (index) => {
setRadioGroupRefreshData(radioGroupRefreshData.map((obj, i) => {
if(i !== index) {
return {text: obj.text, isSelected: false};
}
return {text: obj.text, isSelected: true};
}));
};
return (
<View style={styles.container}>
{
radioGroupRefreshData.map((obj, i) => {
return <RadioButton index={i}
text={obj.text}
isSelected={obj.isSelected}
onClick={handleClick} />
})
}
</View>
);
}
The RadioGroup component has a state variable (an array) called radioGroupRefreshData. when each RadioButton is defined inside the RadioGroup, the handleClick function is passed as a prop in order to be called when a RadioButton is clicked. Here is the code for the RadioButton component:
const RadioButton = (props) => {
const [isSelected, setIsSelected] = useState(props.isSelected);
const initialRenderDone = useRef(false);
useEffect(() => {
if(!initialRenderDone.current) {
initialRenderDone.current = true;
}
else {
props.onClick(props.index);
}
}, [isSelected]);
const handlePress = () => {
if(!isSelected) {
setIsSelected(true);
}
}
return (
<TouchableOpacity style={styles.outsideContainer} onPress={handlePress}>
<View style={styles.radioButtonContainer}>
{ (isSelected) && <RadioButtonInnerIcon width={15} height={15} fill="#04004C" /> }
</View>
<Text style={styles.radioButtonText}>{props.text}</Text>
</TouchableOpacity>
);
}
From what I know, each RadioButton component should re render when the Parent's variable radioGroupRefreshData changes, but the RadioButton component's are not re rendering.
Thank you in advance for any help that you can give me!
Since you have a state in RadioButton you need to update it when the props change. So in RadioButton add useEffect like this:
useEffect(() => {
setIsSelected(props.isSelected);
},[props.isSelected]);
Also you don't have to mix controlled and uncontrolled behaviour of the component: do not set RadioButton state inside RadioButton since it comes from the RadioGroup
So I have created a state like :
const [inState, setInState] = useState([<View />]);
Then on click of some buttons, I am updating inState
const breakOutClick = () => {
setInState([
...inState,
<>
<StatusBoxComponent
ImageConfigIconCheckOut={true}
status={'Break-Out'}
time={time}
/>
</>,
]);
};
const breakInClick = () => {
setInState([
...inState,
<>
<StatusBoxComponent
ImageConfigIconCheckOut={true}
status={'Break-In'}
time={time}
/>
</>,
]);
};
I am able to display everything stored in inState, on this same screen in this manner:
<View>
{inState}
</View>
I am passing this inState to another screen in the following manner:
props.navigation.navigate(ChartScreen, {
inState: Object.assign({}, inState),
});
Then on this second screen, i.e, ChartSCreen, I did the following:
const ChartScreen = (props: any) => {
const {inState} = props.route.params;
useEffect(() => {
console.log('>>>>>>>>>', inState);
}, []);
return (
<View>
{inState} // getting error here
</View>
);
};
I have console the inState, which looks like this:
{
"0": <ForwardRef />,
"1": <React.Fragment>
<StatusBoxComponent
ImageConfigIconCheckOut={true}
status="Break-In"
time="17:51:40"
/>
</React.Fragment>,
"2": <React.Fragment>
<StatusBoxComponent
ImageConfigIconCheckOut={true}
status="Break-Out"
time="17:51:42"
/>
</React.Fragment>
}
How can I display the multiple StatusBoxComponent on my second screen?
You are displaying initially an array, but by calling Object.assign({}, inState) you're creating an object. Where you're getting the error, you're attempting to render that object, not an array of components. Try using Object.values to get only the values and virtually "restore" your initial array.
const ChartScreen = (props: any) => {
const {inState} = props.route.params;
useEffect(() => {
console.log('>>>>>>>>>', inState);
}, []);
return (
<View>
{Object.values(inState)} // getting error here
</View>
);
};
I have a PaymentMethodsScreen screen. On this screen there is a FlatList with PaymentCardItem components inside. And there is a checkbox inside the PaymentCardItem. When this checkbox checked I would like to update selectedCardToken state of PaymentMethodsScreen. But unfortunately I couldn't figure out how to do it. I tried to pass props but I was doing it wrong. Here is my code (without passing props).
How can I achieve that? Thank you very much for your helps.
const PaymentCardItem = ({ family, association, bin_number, token, isSelected }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token) // Something wrong here }
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
const PaymentMethodsScreen = ({navigation}) => {
const {state} = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
add onPress prop to PaymentCardItem:
// PaymentMethodsScreen
<PaymentCardItem
onPress={() => setSelectedCardToken(item.token)}
>
I don't know how the PaymentCardItem component is structured, but generally you should add onPress prop on the TouchableOpacity in the component or whatever is your onPress handler:
// PaymentCardItem component
<TouchableOpacity
onPress={() => props.onPress()}
>
You can pass down the handler function which gets called on checkbox being checked or unchecked to your PaymentCardItem component.
You can also pass setSelectedCardToken directly, but in case you have some extra logic before you update state, it's better to have a handler for more readability.
So, the code will be like below.
const PaymentMethodsScreen = ({ navigation }) => {
const { state } = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const handleCardTokenSelection = (isTokenSelected) => {
if(isTokenSelected) {
setSelectedCardToken(); // whatever logic you have
} else {
setSelectedCardToken(); // whatever logic you have
}
}
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
handleCardTokenSelection={handleCardTokenSelection}
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
const PaymentCardItem = ({ family, association, bin_number, token, isSelected, handleCardTokenSelection }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={handleCardTokenSelection}
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
You need to set the state for PaymentCardItem not for the whole Flatlist, to show the item is selected.
I think you update the PaymentCardItem component to something like the below code(You can update the logic as per requirement)
class PaymentCardItem extends React.Component {
constructor(props) {
super(props);
this.state = {selectedCardToken: "", isSelected: false};
}
setSelectedCardToken=(token)=>{
if(selectedCardToken == token){
this.setState({
selectedCardToken: token,
isSelected: true
})
}
}
render() {
const { family, association, bin_number, token }=this.props;
const { isSelected } = this.state;
return (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token)
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
}
}
In my react-native project, I'm using react-navigation 5 for navigation and react-native-video for a audio/video player.
My requirement is that when a user navigates to another scren, if the audio/video should stop playing. However, that's not happening and the audio keeps playing.
I have created two screens in a stack navigator. The Video Player is a separate component.
Screen Code:
function MainScreen({ navigation }) {
const [audiostatus, setAudioStatus] = useState(true);
React.useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
console.log('Leaving Home Screen');
setAudioStatus(true);
});
return unsubscribe;
}, [navigation]);
return (
<View style={{ flex: 1, justifyContent: 'center',backgroundColor: '#fff' }}>
<Player tracks={TRACKS} paused={audiostatus} />
<Button
title="Go to Screen Without Audio"
onPress={() => navigation.navigate('No Audio Screen')}
/>
<Button
title="Go to Screen With Another Audio (Love Yourself)"
onPress={() => navigation.navigate('Another Audio Screen')}
/>
</View>
);
}
Player Code
Within the Player, I recieve the paused prop to decide whether the video should be already playing or paused. Then the player has controls that control the playbck by changing the state.
export default class Player extends Component {
constructor(props) {
super(props);
this.state = {
paused: props.paused,
totalLength: 1,
currentPosition: 0,
selectedTrack: 0,
repeatOn: false,
shuffleOn: false,
};
}
setDuration(data) {
this.setState({totalLength: Math.floor(data.duration)});
}
setTime(data) {
this.setState({currentPosition: Math.floor(data.currentTime)});
}
seek(time) {
time = Math.round(time);
this.refs.audioElement && this.refs.audioElement.seek(time);
this.setState({
currentPosition: time,
paused: false,
});
}
render() {
const track = this.props.tracks[this.state.selectedTrack];
const video = this.state.isChanging ? null : (
<Video source={{uri: track.audioUrl}} // Can be a URL or a local file.
ref="audioElement"
paused={this.state.paused} // Pauses playback entirely.
resizeMode="cover" // Fill the whole screen at aspect ratio.
repeat={false} // Repeat forever.
onLoadStart={this.loadStart} // Callback when video starts to load
onLoad={this.setDuration.bind(this)} // Callback when video loads
onProgress={this.setTime.bind(this)} // Callback every ~250ms with currentTime
onEnd={this.onEnd}
onError={this.videoError}
style={styles.audioElement}
audioOnly={true} />
);
return (
<View style={styles.container}>
<SeekBar
onSeek={this.seek.bind(this)}
trackLength={this.state.totalLength}
onSlidingStart={() => this.setState({paused: true})}
currentPosition={this.state.currentPosition} />
<Controls
onPressPlay={() => this.setState({paused: false})}
onPressPause={() => this.setState({paused: true})}
paused={this.state.paused}/>
{video}
</View>
);
}
}
The problem is that once a user starts playing the video, and then if he navigates to another screen, the video keeps playing. I want the video to pause. In the screen, i've added useEffect() to set audiostatus to pause on screen blur, but nothing happens. The video keeps playing. Please help.
A simple solution with functional components and hooks is to use
useIsFocused
which returns true or false and re-renders component when changed import it using
import { useIsFocused } from '#react-navigation/native';
const screenIsFocused = useIsFocused();
if you're using "react-native-video" or any other library that takes something like
isPaused
you can use
paused={isPaused || (!screenIsFocused )}
video will only run when it is not paused and the screen is also in focus
Do the following way to pause the video
import React, {useState, useRef} from 'react';
function MainScreen({ navigation }) {
const [audiostatus, setAudioStatus] = useState(true);
// create ref
const playerRef = useRef();
React.useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
console.log('Leaving Home Screen');
setAudioStatus(false);
// new code add to pause video from ref
playerRef.current.pauseVideo();
});
return unsubscribe;
}, [navigation]);
return (
<View style={{ flex: 1, justifyContent: 'center',backgroundColor: '#fff' }}>
<Player ... playerRef={playerRef} />
</View>
);
}
Convert Player class into Hooks as I did
import React, {useState, useImperativeHandle, useRef} from 'react';
function Player = (props) => {
const [paused, setPaused] = useState(props.paused);
const [totalLength, setTotalLength] = useState(1);
const [currentPosition, setCurrentPosition] = useState(0);
const [selectedTrack, setSelectedTrack] = useState(0);
const [repeatOn, setRepeatOn] = useState(false);
const [shuffleOn, setShuffleOn] = useState(false);
const [isChanging, setIsChanging] = useState(false);
const audioElement = useRef(null);
const setDuration = (data) => {
setTotalLength(Math.floor(data.duration));
}
const setTime = (data) => {
setCurrentPosition(Math.floor(data.currentTime));
}
const seek = (time) => {
time = Math.round(time);
audioElement && audioElement.current.seek(time);
setCurrentPosition(time);
setPaused(false);
}
const loadStart = () => {}
// add for accessing ref
useImperativeHandle(props.playerRef, () => ({
pauseVideo: () => setPaused(true),
}));
const track = props.tracks[selectedTrack];
const video = isChanging ? null : (
<Video source={{uri: track.audioUrl}} // Can be a URL or a local file.
ref={audioElement}
paused={paused} // Pauses playback entirely.
resizeMode="cover"
....
onLoadStart={loadStart} // new added
onLoad={setDuration} // new added
/>
);
return (
<View style={styles.container}>
<SeekBar
onSeek={seek}
trackLength={totalLength}
onSlidingStart={() => setPaused(true)}
currentPosition={currentPosition} />
<Controls
onPressPlay={() => setPaused(false) }
onPressPause={() => setPaused(true)}
paused={paused}/>
{video}
</View>
);
}
Your Player appears to only refer to the paused prop only once when it mounts, in the constructor. Player doesn't react or handle any changes to props.paused when it changes in the parent component and is passed after mounting. Implement componentDidUpdate to react to updates to props.paused to update the component state.
export default class Player extends Component {
constructor(props) {
super(props);
this.state = {
paused: props.paused,
totalLength: 1,
currentPosition: 0,
selectedTrack: 0,
repeatOn: false,
shuffleOn: false,
};
}
...
componentDidUpdate(prevProps, prevState) {
const { paused } = this.props;
if (!prevState.paused && paused) {
this.setState({ paused });
}
}
...
render() {
...
const video = this.state.isChanging ? null : (
<Video
...
paused={this.state.paused}
...
/>
);
return (
<View style={styles.container}>
...
{video}
</View>
);
}
}