Application wide Modal in React Native - react-native

I'm currently using react native modal and it serves the purpose of showing modals.
My problem currently is that I want to show the modal application wide. For example when a push notification received I want to invoke the modal regardless of which screen user is in. The current design of the modals bind it to a single screen.
How can this be overcome?

first of all make a context of your modal
const BottomModal = React.createContext();
then provide your modal using reactcontext provider
export const BottomModalProvider = ({children}) => {
const panelRef = useRef();
const _show = useCallback((data, type) => {
panelRef.current.show();
}, []);
const _hide = useCallback(() => {
panelRef.current.hide();
}, []);
const value = useMemo(() => {
return {
_show,
_hide,
};
}, [_hide, _show]);
return (
<BottomPanelContext.Provider value={value}>
{children}
<BottomPanel fixed ref={panelRef} />
</BottomPanelContext.Provider>
);
};
here is code for bottom panel
function BottomPanel(props, ref) {
const {fixed} = props;
const [visible, setVisibility] = useState(false);
const _hide = () => {
!fixed && hideModal();
};
const hideModal = () => {
setVisibility(false);
};
useImperativeHandle(ref, () => ({
show: () => {
setVisibility(true);
},
hide: () => {
hideModal();
},
}));
return (
<Modal
// swipeDirection={["down"]}
hideModalContentWhileAnimating
isVisible={visible}
avoidKeyboard={true}
swipeThreshold={100}
onSwipeComplete={() => _hide()}
onBackButtonPress={() => _hide()}
useNativeDriver={true}
style={{
justifyContent: 'flex-end',
margin: 0,
}}>
<Container style={[{flex: 0.9}]}>
{!fixed ? (
<View style={{flexDirection: 'row', justifyContent: 'flex-end'}}>
<Button
style={{marginBottom: 10}}
color={'white'}
onPress={() => setVisibility(false)}>
OK
</Button>
</View>
) : null}
{props.renderContent && props.renderContent()}
</Container>
</Modal>
);
}
BottomPanel = forwardRef(BottomPanel);
export default BottomPanel;
then wrap your app using the provider
...
<BottomModalProvider>
<NavigationContainer screenProps={screenProps} theme={theme} />
</BottomModalProvider>
...
lastly how to show or hide modal
provide a custom hook
const useBottomPanel = props => {
return useContext(BottomPanelContext);
};
use it anywhere in app like
const {_show, _hide} = useBottomModal();
//....
openModal=()=> {
_show();
}
//...
If you are not using hooks or using class components
you can easily convert hooks with class context
https://reactjs.org/docs/context.html#reactcreatecontext
this way you can achieve only showing the modal from within components
another way is store the panel reference globally anywhere and use that reference to show hide from non-component files like redux or notification cases.

Related

Jest React Native, testing / mocking? onLayout

I currently have this relatively simply screen in React, that I need to test in Jest, however, I'm not terribly familiar with the library and this is what I've got so far.
Things I'd like to test?
The onLayoutEvent. Does this need mocked?
Showing / hiding of the spinner on page. At the moment, it finds it here: (which is fine)
expect(spinner).not.toBeNull();
But still finds it after the event call, whereas in actual fact it is hidden by state after the event fires.
Loading for this component occurs from a call to setLoading in it's children. I've also zero clue on how to test this. Any assistance on what / how to test this component appreciated.
describe('Login', () => {
test('it should render', () => {
renderWithStore(<LoginStarter />);
});
test('it should fire a layout event', async () => {
const { getByTestId } = renderWithStore(<LoginStarter />);
const view = await getByTestId('loginId');
const spinner = await getByTestId('loginSpinner');
expect(spinner).not.toBeNull();
act(() =>
fireEvent(view, 'onLayout', {
nativeEvent: { layout: { width: 500 } },
}),
);
});
});
The core component
const [widthLoading, setWidthLoading] = useState(true);
const [loading, setLoading] = useState(false);
const theme = useTheme();
const [containerWidth, setContainerWidth] = useState<number>();
const onLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
setContainerWidth(width);
setWidthLoading(false);
};
const tabs = [
{
title: 'Login',
component: () => LoginScreen({ componentId: props.componentId, setLoading }),
testID: 'tab1',
},
{
title: 'Sign up',
component: () => RegisterScreen({ componentId: props.componentId, setLoading }),
testID: 'tab2',
},
];
const wrapperStyle = {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
opacity: loading ? 0.5 : 1,
};
const loadingStyle = {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
position: 'absolute',
};
return (
<View testID="loginId" onLayout={onLayout} style={wrapperStyle as StyleProp<ViewStyle>}>
{loading && (
<View style={loadingStyle as StyleProp<ViewStyle>}>
<ActivityIndicator size="large" color={theme.spinner} />
</View>
)}
<View>
{!widthLoading && containerWidth ? (
<Tabs style="light" tabWidth={Math.round(containerWidth / 2)} tabs={tabs} tabsScrollEnabled />
) : (
<ActivityIndicator testID="loginSpinner" size="large" color={theme.spinner} />
)}
</View>
</View>
);
I'm struggling with testing onLayout as well. One thing I will point out is that in your code
act(() =>
fireEvent(view, 'onLayout', {
nativeEvent: { layout: { width: 500 } },
}),
);
onLayout should actually just be layout. I dug into the library code and found this:
const toEventHandlerName = eventName => `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
Also, you don't need to wrap fireEvent with act as it is wrapped by act by default. As per the docs:
Act: Useful function to help testing components that use hooks API. By default any render, update, fireEvent, and waitFor calls are wrapped by this function, so there is no need to wrap it manually.
All that said, I still can't get onLayout to fire.
Edit:
The above actually was triggering the onLayout for me. It was the was I was spying on useState that was my issue.

you need to specify name or key when calling navigate with an object as the argument

i'm having an messages screen and i need to navigate to a "single message" when tapping to the List item of messages but i get this error "you need to specify name or key when calling navigate with an object as the argument"
i have created the "single message" screen and added it as a <Stack.Screen/> also but i don't know what i'm doing wrong.
below is my code:
function MessagesScreen({navigation}) {
const [messages, setMessages] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const loadMessages = async () => {
const response = await messagesApi.getMessages();
setMessages(response.data);
}
useEffect(() => {
loadMessages();
}, []);
const handleDelete = message => {
setMessages(messages.filter((m) => m.id !== message.id));
}
return (
<Screen>
<FlatList
data={messages}
keyExtractor={message => message.id.toString()}
renderItem={({ item }) =>
<ListItem
title={item.fromUserId}
subTitle={item.content}
image={item.image}
onPress={() => navigation.navigate(routes.MESSAGE_SINGLE, item)}
renderRightActions={() =>
<ListItemDeleteAction onPress={() => handleDelete(item)} />}
/>
}
ItemSeparatorComponent={ListItemSeparator}
refreshing={refreshing}
onRefresh={() => {
setMessages([
{
id: 1,
title: 'T1',
description: 'D1',
image: require('../assets/mosh.jpg')
},
])
//setMessages(loadMessages());
}}
/>
</Screen>
);
}
const styles = StyleSheet.create({
})
export default MessagesScreen;
when i'm logging the "onPress" event on the console like this:
onPress={() => console.log('message selected', item)}
heres what i get:
and below is the MessageSingle screen i created to render the message but i dont know how to do it.
function MessageSingle() {
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}
const styles = StyleSheet.create({
container: {}
});
export default MessageSingle;
so i want to get the message from the list of the messages. maybe i dont have to create e separate screen? i'm a beginner on this
any help would be appreciated!
you need to first add your MessageSingle component to the navigation container. Just put it as one of the screens along your MessagesScreencomponent. Then you need to navigate to it using that name:
onPress={() => navigation.navigate('MessageSingle', {item})}
the above will navigate to the screen with name MessageSingle, and passing the object item as a param.
in order to access this in your MessageSingle component, you need to use the route props.
function MessageSingle({route}) {
console.log('item = ', route.params?.item); // this would be your item.
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}

React-Native FlatList item clickable with data to another screen

I'm trying to access a screen when you click on an item in my flatlist by passing the date I retrieved from the firebase before, I've tried several things without success so I come to you.
Basically when I click on one of the elements -> A screen with details should appear.
export default function Notifications() {
const dbh = firebase.firestore();
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [deliveries, setDeliveries] = useState([]); // Initial empty array of users
useEffect(() => {
const subscriber = dbh
.collection("deliveries")
.onSnapshot((querySnapshot) => {
const deliveries = [];
querySnapshot.forEach((documentSnapshot) => {
deliveries.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setDeliveries(deliveries);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
style={{ flex: 1 }}
data={deliveries}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => { * HERE I NEED TO PASS DATA AND SHOW AN ANOTHER SCREEN FOR DETAILS * }}>
<View style={styles.container}>
<Text>DATE: {item.when}</Text>
<Text>ZIP DONATEUR: {item.zip_donator}</Text>
<Text>ZIP BENEFICIAIRE: {item.zip_tob_deliv}</Text>
</View>
</TouchableOpacity>
)}
/>
);
}
EDIT: Small precision this screen is located in a Tab.Navigator
you can pass params in navigation,
export default function Notifications(props) {
const { navigation } = props
const dbh = firebase.firestore();
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [deliveries, setDeliveries] = useState([]); // Initial empty array of users
useEffect(() => {
const subscriber = dbh
.collection("deliveries")
.onSnapshot((querySnapshot) => {
const deliveries = [];
querySnapshot.forEach((documentSnapshot) => {
deliveries.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setDeliveries(deliveries);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
style={{ flex: 1 }}
data={deliveries}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
navigation.navigate('screenName', {
//pass params here
})
}}>
<View style={styles.container}>
<Text>DATE: {item.when}</Text>
<Text>ZIP DONATEUR: {item.zip_donator}</Text>
<Text>ZIP BENEFICIAIRE: {item.zip_tob_deliv}</Text>
</View>
</TouchableOpacity>
)}
/>
);
}
you can access params in the navigated screen by props.route.params

React native button click render different components

I am using Expo React native for my app. I have created three buttons. When user will click the button it will fetch data and render it in one screen. Each button will button will fetch different api. Under the buttons there will be flatlist, where it will display the data. I can do with one button click display the data but I could not figure how to display other buttons api. I share my code in codesandbox. ps: Below code make this app get super slow
This is my code
import React from 'react';
import { Text, View, ScrollView } from 'react-native';
import styled from 'styled-components';
import PressableButton from './Button';
import axios from 'axios';
const api = "https://jsonplaceholder.typicode.com/users";
const anApi = "https://jsonplaceholder.typicode.com/photos"
const Data = ({ data }) => {
return (
<View style={{ flex: 1 }}>
<Text>{JSON.stringify(data, null, 4)}</Text>
</View>
)
}
const AnData = ({ andata }) => {
return (
<View style={{ flex: 1 }}>
<Text>{JSON.stringify(andata, null, 1)}</Text>
</View>
)
}
export default function App() {
const [data, setData] = React.useState([]);
const [anotherdata, setanotherData] = React.useState([]);
const updateState = async () => {
await axios(api)
.then((res) => {
setData(res.data);
})
.catch((err) => {
console.log("failed to catch", err);
});
};
const anoThereState = async () => {
await axios(anApi)
.then((res) => {
setanotherData(res);
})
.catch((err) => {
console.log("failed to catch", err);
});
};
return (
<React.Fragment>
<Container>
<PressableButton onPress={updateState} title='First button' bgColor='#4267B2' />
<PressableButton onPress={anoThereState} title='Second button' bgColor='lightblue' />
<PressableButton onPress={() => true} title='Third button' bgColor='#4267B2' />
</Container>
<Scroll>
{data && data === undefined ? <Text>loading</Text> : <Data data={data} />}
{anotherdata && anotherdata === undefined ? <Text>loading</Text> : <AnData andata={anotherdata} />}
</Scroll>
</React.Fragment>
);
}
const Container = styled.View`
flex-direction: row;
justify-content: center;
padding: 70px 0px 20px 0px;
`;
const Scroll = styled.ScrollView`
flex: 1;
`
Under the buttons there will be flatlist
You're not using FlatList, only showing response in text and the data for second button is huge that's why It's super slow.
Here are the changes I made, check if this is what you're looking for?
Also if you want one data to show at a time you can either use tabs or show/hide the data depending on selection like I've done in code.

React Navigation 5 headerRight button function called doesn't get updated states

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.
What changes need to be made to the navigation headerRight button to fix this issue?
Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.
import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
const submit = () => {
//label doesn't return the updated state here
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}
Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation,label]);
Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.
To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
const [saving, setSaving] = useState(false);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={() => setSaving(true)}>
<Text>Done</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
useEffect(() => {
// Check if saving to avoid calling submit on screen unmounting
if(saving){
submit()
}
}, [saving]);
const submit = () => {
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}