test Image onError call in react native - image

I have a component that wraps Image component and calls an internal function when Image's on error gets called
render() {
const {
avatars, userId, size, containerStyle, onPress, currentUserId,
} = this.props
const { hasError } = this.state
this.onError = this.onError.bind(this)
const id = userId || currentUserId
return (
<TouchableWithoutFeedback onPress={onPress}>
<Image
source={avatars && avatars[id] && !hasError
? { uri: avatars[id].localPath } : defaultAvatar}
style={{
width: size, height: size, borderRadius: size / 2, ...containerStyle,
}}
onError={this.onError}
/>
</TouchableWithoutFeedback>
)
}
}
Now I want to test my components this.onError function works correctly, is there a way to mock react-native image component and force it to run the onError function so I can test my fuction using jest?

In short, this is how you want to trigger onError function in your test
fireEvent(getByTestId("img"), "error");
You can of course choose to use any other getter instead of getByTestId if you want, but I prefer to have testId's in most cases.
Below I have explained it with a more detailed example.
If my component is this :
const DisplayAnImage = () => {
return (
<Image
testID="imgTest"
style={{width: 50, height: 50}}
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
}}
/>
);
}
export default DisplayAnImage;
Then I would trigger onError function like this in my test :
test("test to trigger on error function", async () => {
const { getByTestId } = render(<DisplayAnImage />);
fireEvent(getByTestId("imgTest"), "error");
// write your assertions here
});

Just add a invalid uri in source prop.
e.g
Just replace
uri: avatars[id].localPath
with
uri: avatars[id].localPath + ".random"

I found the best solution was just to simulate the error callback like this
wrapper.find('Image').simulate('error')
expect(wrapper.state('hasError')).toEqual(true)

Related

Setting the state from a callback in the parent component doesn't work

I'm trying to set the state from the parent component using a callback. This callback gets passed down to the child component that renders a material ui datatable. The callback responds onClick and passes some values to the callback. The problem is that setting the state with the values from the callback arguments doesn't work.
My assumption is that when the user clicks the button from the child component, it should invoke the callback function and pass the values I needed to set the state.
Parent Component:
export default function ViewJobs() {
const [type, setType] = useState('');
const [params, setParams] = useState({});
const callback = ({ cellValues, componentType, path }) => {
setType(componentType);
setParams(cellValues) // Sets the params with an object.
console.log(cellValues) // Displays the data I need in the console
history.push(path);
};
console.log(params) // Displays undefine in the console.
return(
<React.Fragment>
<TabPanel value={value} index={0} dir={theme.direction} >
<DataTable
jobs={job}
title='All'
parentCallback={callback}
/>
</TabPanel>
</React.Fragment>
);
}
Child Component
import React, { useEffect } from 'react';
export default function DataTable(props) {
const { jobs, parentCallback } = props;
const rows = jobs.payload;
const handleDiaryClick = (event, cellValues) => {
const params = {
cellValues,
componentType: 'diary',
path: "/view/jobs/diary"
};
parentCallback(params);
};
const renderDiaryElement = params => {
return (
<Button
variant="contained"
color="primary"
style={{ backgroundColor: "#000000" }}
onClick={(event) => {
handleDiaryClick(event, params);
}}
>
<MenuBookIcon />
</Button>
);
}
return (
<div
className={classes.root}
style={{ height: 400, width: '100%' }}
>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
//checkboxSelection
disableSelectionOnClick
/>
</div>
);
}
Since the state has been lifted up to the parent component, I'm under the impression that the code above should be working.
I tried to reproduce the issue but I couldn't replicate it.
Any advice or inputs are appreciated. Thanks.
After further checking on my codebase, I found that the history.push(path) located in my callback is causing the issue. I had to remove this line of code for it to work.

React Navigation header button that controls rendering of the screen

PLEASE NOTE that 'this' is not accessible from a static function: React Native : Access Component state inside a static function
I am trying to define a button in the screen's header that, when clicked, will affect rendering, and will be replaced with another icon.
It is an old app, still using react navigation 3.
I didn't know how to do the following things:
modify the component's state from a function that is activated when the button is pressed
modify the screen parameter from this function
What I managed to implement is the following lame and embarrassing way to do it:
When the button is clicked, a static function is executed, which modfies a static variable
periodic code is fired in componentDidMount that checks whether the static detailedDisplay variable has been modified. If it has been modified, this code sets a state variable that affects rendering. This periodic code also modifies the screen parameter which changes the icon in the header (because, as I wrote above, I also failed to set the parameter from the static function).
How can this be done in not-so-lame way?
Here is my code:
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/Octicons';
...
export default class Messages extends Component {
...
var detailedDisplay = false;
...
static navigationOptions = ({ navigation }) => {
return {
headerRight: //navigation.getParam('detailedDisplay', false) ?
detailedDisplay ?
<TouchableOpacity onPress={() => this.toggleDisplay(navigation)}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => this.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
static toggleDisplay(navigation) {
detailedDisplay = !detailedDisplay;
// the following statement gave the error "cannot read property
// 'setParams' of undefined", so I am setting it below.
// navigation.setParams({ detailedDisplay });
}
constructor(props) {
super(props);
this.state = {
detailedDisplay: false,
};
}
componentDidMount() {
setInterval(() => {
if (detailedDisplay !== this.state.detailedDisplay) {
this.props.navigation.setParams({ detailedDisplay });
this.setState({ detailedDisplay });
}
}, 500);
}
...
return (
<View>
{ this.state.detailedDisplay ?
{this.renderConcise()} :
{this.renderDetailed()}
}
</View>
);
}
renderDetailed() {
...
}
renderConcise() {
...
}
}
I think you can try to use your state inside your navigationOption method
static navigationOptions = ({ navigation }) => {
return {
headerRight: //navigation.getParam('detailedDisplay', false) ?
this.state.detailedDisplay ?
<TouchableOpacity onPress={() => this.toggleDisplay(navigation)}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => this.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
And then change your state in the toggleDisplay method
static toggleDisplay(navigation) {
this.setState(state=> detailedDisplay:!state.detailedDisplay)
// the following statement gave the error "cannot read property
// 'setParams' of undefined", so I am setting it below.
// navigation.setParams({ this.state.detailedDisplay });
}
You can try and keep the param and a state variable in sync.
An example POC is here
Here, I have tried to keep the state toggleVariable and the param value in sync but updating them through a common setter, and on Component Mount it will just be the same as the parameter.
You cannot read properties of this because this in JS is dynamic, and when you pass function as callback this is lost. There is 2 solutions that i know:
One of them bind this using bind method:
constructor(props) {
super(props);
this.state = {
detailedDisplay: false,
};
this.toggleDisplay = this.toggleDisplay.bind(this);
this.navigationOptions = this.navigationOptions.bind(this)
}
Now you can use non-static methods and you can access react state and methods in them:
navigationOptions({ navigation }) {
// somewhere in code <button onPress={this.toggleDisplay}><button>
// this.props this.state this.setState is available here
};
toggleDisplay(navigation) {
// this.props this.state this.setState is available here
// this.props.navigation.setParams() is available too
}
Finally, you can change the detailedDisplay state and screen params in toggleDisplay function. So when you'll press button state'll be changed and your commponent'll be re-rendered.
Additional
Second way to do it is to call method in a function:
onPress={() => this.toggleDiaplay()}
Here you must also use non-static methods, and this will work the same way as the first solution with bind.
Try doing something like this:
UPDATED: removed static from the toggleDisplay and added Class reference in the static function.
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/Octicons';
...
export default class Messages extends Component {
...
state = {
detailedDisplay: false,
};
...
static navigationOptions = ({ navigation }) => {
return {
headerRight: navigation.getParam('detailedDisplay', false) ?
<TouchableOpacity onPress={() => Messages.toggleDisplay()}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => Messages.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
toggleDisplay() {
this.setState({detailedDisplay: !this.state.detailedDisplay})
this.props.navigation.setParams({detailedDisplay: true});
}
constructor(props) {
super(props);
}
componentDidMount() {
// setInterval(() => {
// if (detailedDisplay !== this.state.detailedDisplay) {
// this.props.navigation.setParams({ detailedDisplay });
// this.setState({ detailedDisplay });
// }
// }, 500);
}
...
return (
<View>
{ this.state.detailedDisplay ?
{this.renderConcise()} :
{this.renderDetailed()}
}
</View>
);
}
renderDetailed() {
...
}
renderConcise() {
...
}
}

Formik's use with react-query onSubmit throwing "Invalid hook call"

I have a React Native form that I'm trying to use Formik with react-query.
The problem is using useQuery() in a function called from onSubmit I am getting hook errors:
Warning: An unhandled error was caught from submitForm() [Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I believe I understand what the error is but I don't understand how to do a workaround to get this working.
The example I threw together just to demonstrate the issue:
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { Button, StyleSheet, Text, TextInput, View } from 'react-native';
import { useQuery } from 'react-query';
import { Formik } from 'formik';
import axios from 'axios';
const getPokemonList = async () => {
const { data } = await axios.get("https://pokeapi.co/api/v2/pokemon");
return data;
};
function authenticate(username, password) {
const { isLoading, error, data } = useQuery('fetchLuke', getPokemonList);
if (data) {
{
return (
<Text>
{JSON.stringify(data, null, 2)}
</Text>
);
}
}
if (error) {
return (
<Text>{error}</Text>
);
}
if ( isLoading ) {
return (
<Text>Retrieving Luke Skywalker Information...</Text>
);
}
}
export default function App() {
return (
<View style={styles.container}>
<Formik
initialValues={{ email: '' }}
onSubmit={(values, actions) => {
authenticate(values.email);
actions.resetForm();
}}
>
{({
handleChange,
handleBlur,
handleSubmit, values }) => <View>
<TextInput
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
<Button onPress={handleSubmit} title="Submit" />
</View>}
</Formik>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
This is fairly straightforward.
Create a variable and assign useQuery to it (in App, because useQuery is a hook and hooks can only be used in functional components)
Call the variable in your onSubmit or pass it to authenticate...
react-query has a dependent query feature where the query only runs when a custom condition is satisfied.
Check the example below from https://react-query.tanstack.com/guides/dependent-queries
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)

Image fails to render after successful fetch in React native

I'm attempting to render an image that is the result of a successful fetch.
It seems fairly simple and everything works, except the picture fails to render.
In my constructor:
constructor(props) {
super(props);
const deliveryManPic = Config.images.deliveryManPic;
this.state = {
callModal: false,
beepModal: false,
pictureForBackground: null,
finished: deliveryManPic,
secondPic: null,
};
this.batteryLevelIndicator = '';
this.pictureUrl = 'someURL';
}
In my ComponentWillMount:
componentWillMount() {
const urlForPicture = this.pictureUrl;
apiService.getPicture(urlForPicture, (res) => {
if (res !== null) {
// console.log('here is your response', res);
this.setState({ pictureForBackground: res, });
} else {
loggingService.debug('theres been a problem');
}
});
}
Finally, inside my render method:
<View style={styles.deliveryGuyView}>
<Image
resizeMode="cover"
style={styles.policeCarIcon}
defaultSource={{ uri: this.state.finished }}
source={{ uri: this.state.pictureForBackground }}
onLoadEnd={() => {
console.log('pic has finished loading');
}}
/>
</View>
I don't see any yellow messages or errors in the console.log, but I don't see the picture that was successfully fetched. What could be the reason the picture fails to render?
I forgot to add the height and width inside the PoliceCarIcon styles object.

Efficiently update all items in large flat list in react native

I'm writing an image picker using react-native's CameraRoll API and rendering them in a FlatList inside CameraRollScreen component. This component takes a prop called maxPhotos, say 3, when a user has selected 3 photos, all other photos will be disabled (cannot be selected anymore), it looks like this (this is what I have right now, it works, but not performant):
As you can see, when I've selected 3 photos (which is the limit), all other photos are covered by a transparent view (disabled). This is not performant, doesn't seem so in the GIF, but when running on a real device, this problem can no longer be ignored. Selecting the first 2 photos doesn't cause any lag, however, upon selecting the last photo, since all other photos will have to be disabled, it becomes laggy. But I have no idea how else I could disable the other photos without disabling them 1 by 1. Here is the code I have for my image picker:
Since every image has different states, I also make each photo a PureComponent called CameraRollImage that has the following state:
{
uri: '',
index: -1 // if not selected, it's -1, if selected, it denotes
// the position of the photo in the 'selectedPhotos'
// array
disabled: false // Whether it should be disabled
}
CameraRollImage component:
class CameraRollImage extends PureComponent {
constructor(props) {
super(props);
this.state = {
uri: '',
index: -1,
disabled: false
};
this.onSelectPhoto = this.onSelectPhoto.bind(this);
}
componentWillMount() {
const { uri, index, disabled } = this.props;
this.setState({ uri, index, disabled });
}
componentWillReceiveProps(nextProps) {
const { uri, index, disabled } = nextProps;
this.setState({ uri, index, disabled });
}
onSelectPhoto() {
const { uri, index } = this.state;
this.props.onSelectPhoto({ uri, index });
// 'onSelectPhoto' is a method passed down to each photo
// from 'CameraRollScreen' component
}
render() {
const { uri, index, disabled } = this.state;
return (
<View style={{ ... }}>
<TouchableOpacity
disabled={disabled}
onPress={this.onSelectPhoto}
>
<Image
source={{ uri }}
style={{ ... }}
/>
</TouchableOpacity>
// If disabled, render a transparent view that covers the photo
{disabled && <View
style={{
position: 'absolute',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
width: ... height: ...
}}
/>}
// render the index here
</View>
);
}
}
export default CameraRollImage;
Then, in CameraRollScreen Component:
class CameraRollScreen extends Component {
constructor(props) {
super(props);
this.state = {
allPhotos: [], // all photos in camera roll
selectedPhotos: []
};
this.onSelectPhoto = this.onSelectPhoto.bind(this);
this.renderPhoto = this.renderPhoto.bind(this);
}
componentWillMount() {
// Access the photo library to grab all photos
// using 'CameraRoll' API then push all photos
// to 'allPhotos' property of 'this.state'
}
onSelectPhoto({ uri, index }) {
let { selectedPhotos } = { ...this.state };
if (index === -1) {
// this means that this photo is not selected
// and we should add it to 'selectedPhotos' array
selectedPhotos.push(uri);
} else {
_.pullAt(selectedPhotos, index);
}
this.setState({ selectedPhotos });
}
renderPhoto({ item }) {
// item is the uri of the photo
const { selectedPhotos } = this.state;
const index = _.indexOf(selectedPhotos, item);
// A photo should be disabled when reach the limit &&
// it's not selected (index === -1)
return (
<CameraRollImage
uri={item}
index={index}
onSelectPhoto={this.onSelectPhoto}
disabled={index === -1 && selectedPhotos.length >= 3}
/>
);
}
render() {
const { allPhotos } = this.state;
return (
<FlatList
data={allPhotos}
extraData={this.state}
...
...
numColumns={3}
renderItem={this.renderPhoto}
/>
);
}
}
export default CameraRollScreen;
I have only 100 photos in my photo library and it's already causing lags, many people have way way way more photos than I do, this way will cause disaster, but how should I go about updating so many photos in FlatList? Or, should I use FlatList at all?
Found the solution, thanks to Pir Shukarullah Shah and RaphaMex.
If I scroll down fast enough, many images were not rendered and they are being rendered when I reach them. This seems right, why render them anyway when they're not on the screen? What I did was that I made use of onViewableItemsChanged of FlatList:
<FlatList
...
...
keyExtractor={(item) => item} // This is important!!!
onViewableItemsChanged={this.onViewablePhotosChanged}
initialNumberToRender={Math.ceil(SCREEN_HEIGHT / IMAGE_SIZE) * 3}
...
/>
Then, onViewablePhotosChanged method:
onViewablePhotosChanged({ viewableItems }) {
let viewablePhotos = [];
viewableItems.forEach((item) => viewablePhotos.push(item.key));
this.setState({ viewablePhotos });
// Here, every object in 'viewableItems' has a key, which
// is the key you provided in 'keyExtractor={(item) => ...}',
// I used the 'uri' of each photo as the key, that's why
// I am pushing viewable photos' uri's to 'viewablePhotos' array
}
Lastly, modify the renderPhoto function to pass a viewable prop
renderPhoto({ item }) {
...
...
return (
<CameraRollImage
...
...
viewable={_.include(this.state.viewablePhotos, item)}
/>
);
}
Then, in CameraRollImage component, where we render images, there is a prop called viewable, if viewable === false, we simply do not update it:
componentWillReceiveProps(nextProps) {
const { ..., ..., viewable } = nextProps;
if (!viewable) {
this.setState({ viewable: false });
return;
}
...
...
}
BETTER YET!!! if viewable is false, instead of rendering the image, we render an equal-sized empty view, you know, to save memory, which of course doesn't seem to be important if there're only 100 photos:
render() {
if (!this.state.viewable) {
return (
<View
style={{
width={IMAGE_SIZE}
height={IMAGE_SIZE}
}}
/>
);
}
return (
<Image
...
...
/>
);
}

Resources