How to access value calculated in `useEffect` hook from renderer - react-native

I am developing a React-Native project with functional component.
Here is a very simple screen which renders a calculated result list. Since I need to calculation to be called only once so I put it inside the useEffect hook.
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
As you can see I have called the doCalculation() to get the result inside useEffect hook. My question is how can I render the result in the return part? Since the result is calculated inside the hook, it is not accessible in the renderer.
P.S. Moving the const result = doCalculation() outside the useEffect hook is not an option since I need the calculation to be called only once.

Below is an example. According to the above comments it looks like you want it to be called once on component mount. All you really need to do is add a useState
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [calculatedData, setCalculatedData] = useState([])
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
setCalculatedData(result)
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{calculatedData.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
const [calculatedData, setCalculatedData] = useState([])
useState is a hook used to store variable state. When calling setCalculatedData inside the useEffect with empty dependency array it will act similar to a componentDidMount() and run only on first mount. If you add variables to the dependency array it will re-run every-time one of those dep. change.
You can change the data inside the calculatedData at anytime by calling setCalculatedData with input data to change to.

Make use of useState to save the calculation result and then use the variable inside return. See https://reactjs.org/docs/hooks-state.html.

Code snippet:
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [result, setResult] = useState([]);
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const tempRes = doCalculation();
setResult(tempRes);
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;

Is async function?
if the function is not async (not wating for respond like from api) - you don't need useEffect.
import React from 'react';
import { Text, View } from 'react-native';
import {doCalculation} from '../util/helper'
const results = doCalculation();
const MyScreen = () => {
return (
<View>
{results.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
else you should wait until the results come from the server..
import React, { useState, useEffect } from 'react';
import { Text, View } from 'react-native';
import { doCalculation } from '../util/helper';
const MyScreen = () => {
const [results, setResults] = useState(null) // or empty array
useEffect(() => {
(async () => {
setResults(await doCalculation());
})();
}, []);
return (
<View>
{results?.map(item => <Text key={item.id}> {item.value} </Text>) || "Loading..."}
</View>
)
}
export default MyScreen;
and I can use more readable code:
if (!results) {
return <View>Loading...</View>
}
return (
<View>
{results.map(item => <Text key={item.id}> {item.value} </Text>)}
</View>
)
the async function can be like:
const doCalculation = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, value: 1 }]);
}, 2000);
});
};

Related

How to hide element when device keyboard active using hooks?

I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251
So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.
When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:
ExceptionsManager.js:179 Warning: Functions are not valid as a React
child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than
return it.
in RCTView (at View.js:34)
Question:
How can you attach keyboard displayed listener to a component in the render?
Example useHideWhenKeyboardOpen function:
import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';
// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
// todo: finish refactoring.....
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = () => {
setIsKeyboadVisible(true);
};
const _keyboardDidHide = () => {
setIsKeyboadVisible(false);
};
/**
* Add callbacks to keyboard display events, cleanup in useeffect return.
*/
useEffect(() => {
console.log('isKeyboadVisible: ' + isKeyboadVisible);
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeCurrentListener();
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};
export default useHideWhenKeyboardOpen;
Example Usage:
return(
.
.
.
{useHideWhenKeyboardOpen(
<View style={[styles.buttonContainer]}>
<Button
icon={<Icon name="save" size={16} color="white" />}
title={strings.STOCKS_FEED.submit}
iconRight={true}
onPress={() => {
toggleSettings();
}}
style={styles.submitButton}
raised={true}
/>
</View>,
)}
)
Mindset shift will help: think of hooks as data source rather than JSX factory:
const isKeyboardShown = useKeyboardStatus();
...
{!isKeyboardShown && (...
Accordingly your hook will just return current status(your current version look rather as a HOC):
const useHideWhenKeyboardOpen = () => {
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = useCallback(() => {
setIsKeyboadVisible(true);
}, []);
const _keyboardDidHide = useCallback(() => {
setIsKeyboadVisible(false);
}, []);
useEffect(() => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible;
};
Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.

Can an independent functional component re-render based on the state change of another?

I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.

React Native Testing Library get by access role

I am really newReact Native Testing Library. My app basically works like this: it fetched the data and display on my to my as Text format, I used jsonplace holder api. This is app-demo. I have have created one Text where I define test role="header". I want to test the Text, does it work properly under role="header". I make a fake data and try to test it. I can able target the role from the component but I don't how to get the expected data. I tried with toBe, getByText but each time I am getting error: TypeError: toBe is not function.
This is my app component
const [state, setState] = React.useState([]);
React.useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos/")
.then((response) => response.json())
.then((json) => setState(json));
}, []);
return (
<View style={styles.container}>
{state.map((i) => (
<Text role:'header'>{i.title}</Text>
))}
</View>
);
}
This is my test suite
import React from 'react';
import { fireEvent, render, cleanup, act } from '#testing-library/react-native';
import Json from './Json';
describe('<Json/>', () => {
afterEach(cleanup);
test('get data properly', async () => {
const component = <Json/>;
const { getByA11yRole } = render(component);
const header = await getByA11yRole('header');
console.log(header);
expect(header).toBe(/delectus aut autem/);
});
});

React Native GraphQL nested data array returning error

I have tried everything I can think of to solve this and am still stumped. I am using AWS AppSync GraphQL to store a dataset that I would like to call into a SectionList.
For the SectionList I am using a hardcoded id to call the data set through a GraphQL query. The SectionList displays correctly when I am using dummy data. It also displays the 1-to-1 relationships in the API correctly.
I already configured amplify to increase the statement depth and I can see the data in the Object.
Code for the SectionList
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Text, Image, ImageBackground, ScrollView, TouchableOpacity, SectionList, SafeAreaView } from 'react-native';
import Feather from 'react-native-vector-icons/Feather';
import AntDesign from 'react-native-vector-icons/AntDesign';
import { API, graphqlOperation } from 'aws-amplify';
import { getGame, listGameSections, listGames } from '../graphql/queries';
const Item = ({ title }) => (
<View>
<Text>
{title}
</Text>
</View>
);
const GameScreen = ({ navigation }) => {
const [game, setGame] = useState([]);
useEffect(() => {
const fetchGame = async () => {
const gameInfo = { id: '0e2cb273-b535-4cf7-ab16-198c44a4991c'};
if (!gameInfo) {
return;
}
try {
const response = await API.graphql(graphqlOperation(getGame, {id: gameInfo.id}))
setGame(response.data.getGame);
console.log(response);
} catch (e) {
}
};
fetchGame();
}, [])
return (
<SafeAreaView>
<View>
<Text>
{game.name}
</Text>
</View>
<SectionList
sections={game.sections.items}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<View>
<Text>{title}</Text>
</View>
)}>
</SafeAreaView>
)
};
export default GameScreen;
Log of the object.
I am attempting to display the getGame.sections.items array but am returning an error undefined is not an object. Cannot read property items of undefined.
Please help, I am so stumped now. When I call game.name earlier in the function it displays correctly, but game.sections.items throws an error in the SectionList that it is undefined.
Xadm, you pointed me in the right direction. I added this to my code:
const [game, setGame] = useState({});
const [gameSection, setGameSection] = useState([]);
and in my useEffect:
setGameSection(response.data.getGame.sections.items)
When calling the data, game.name wanted an object, while game.sections.items wanted an array for the SectionList. Adding 2 different functions for each initial states, one for the objects and one for the array, was able to fix the problem and render the data.

test content of a Text element in a stateful component

I am using react-native-testing-library. My component is quite simple:
import React, {Component} from 'react';
import {Text, View} from 'react-native';
import {information} from './core/information';
export default class Logo extends Component {
constructor() {
super();
this.state = {
name: ''
};
information()
.then((details) => {
this.setState({
name: details['name']
});
})
.catch((e) => {
console.log(e);
});
}
render() {
return (
<>
<View>
<Text>{this.state.name}</Text>
</View>
</>
);
}
}
I want to make sure contains the right content. I tried the following but it is failing:
import * as info from "./lib/information";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
spy.mockResolvedValue(Promise.resolve(data));
const {queryByText, debug} = render(<Logo />);
expect(queryByText(data.name)).not.toBeNull();
expect(spy).toHaveBeenCalled();
});
I can confirm the function information() was spied on correctly but still debug(Logo) shows the Text element with empty string.
If it's correctly spying you can try this. I encourage you to use the testID props for the components
render() {
return (
<>
<View>
<Text testID="logo-text">{this.state.name}</Text>
</View>
</>
);
}
import * as info from "./lib/information";
import { waitForElement, render } from "react-native-testing-library";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
//this is already resolving the value, no need for the promise
spy.mockResolvedValue(data);
const {getByTestId, debug} = render(<Logo />);
//You better wait for the spy being called first and then checking
expect(spy).toHaveBeenCalled();
//Spy function involves a state update, wait for it to be updated
await waitForElement(() => getByTestId("logo-text"));
expect(getByTestId("logo-text").props.children).toEqual(data.name);
});
Also, you should move your information call inside a componentDidMount