Prevent an element from rerendering on setState in React Native - react-native

I am using setState in order to dynamically update an image's source when a button is pushed in React Native. However, I am also using the TypeWriter library which types out text with a special 'typewriter' animated effect.
When my setState is called to change the element and the page is rerendered, TypeWriter types the text out again. I don't want this. Is there a way to exclude my TypeWriter text from being rerendered?
Code snippet:
export const AccountScreen = ({ navigation }) => {
this.state = {
img1Src: require('../assets/img/token.png')
}
const [state, setState] = useState(this.state);
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//

export const NewComponent = ({ navigation }) => {
const [state, setState] = useState({
img1Src: require('../assets/img/token.png')
});
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//
You can make a new component is called NewComponent. Then,
export const AccountScreen = ({ navigation }) => {
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<NewComponent />
etc...//
Also, you cannot use this in the functional component. In addition, if you want to call a function in functional component, you must use it with useCallback.

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.

React initial value of createContext is used instead of provided one

I'm creating a Context with the boolen isDark inside my App. The boolean isDark is created with useState and I provide this boolean and a function to change the boolean to a ThemeContext to access it further down the component tree.
Down below I'm creating the ThemeContext with the boolean initialized to false and a function that just warns in the console that the initial value is being used:
//ThemeContext.tsx
export type ContextType = {
isDark: boolean
toggleTheme: () => void
}
const ThemeContext = createContext<ContextType>({
isDark: false,
toggleTheme: () => console.warn('Still using initial value'),
})
export const useTheme = () => useContext(ThemeContext)
export default ThemeContext
Here I'm providing the theme and the functionality to change it through the toggleTheme function:
//CustomThemeProvider.tsx
export const CustomThemeProvider: React.FC = ({ children }) => {
const [isDark, setDark] = useState(false)
const toggleTheme = () => {
console.log('Change theme')
setDark(!isDark)
}
const providerTheme = useMemo(
() => ({ isDark, toggleTheme }),
[isDark, toggleTheme],
)
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<ThemeContext.Provider value={providerTheme}>
{children}
</ThemeContext.Provider>
</ThemeProvider>
)
}
I now want to access the boolean and the toggleTheme function and do that through my custom hook (useTheme) created at the start, that just uses useContext:
//App.tsx
export default function App() {
const { isDark, toggleTheme } = useTheme()
return (
<CustomThemeProvider>
<Box flex={1} justifyContent="center">
<Paper title="Test Title">
<Switch onValueChange={toggleTheme} value={isDark} />
</Box>
</CustomThemeProvider>
)
}
When I now try to switch the theme with the Switch component (React Native), I get the console warning that my initial function is being called. That means that my toggleTheme function is still the initial function () => console.warn('Still using initial value') even though I provided a new function, that should change the isDark boolean with my ThemeContext.Provider.
Why is my inital function still being called by the Switch instead of my provided one to change the theme?
Your useTheme() is getting the value from the default state since a Provider above it in the component tree is not found (it is at the same level).
Just wrap your application with your CustomThemeProvider (or a level above):
ReactDOM.render(
<CustomThemeProvider>
<App />
</CustomThemeProvider>,
document.getElementById('root')
);
Be careful too with the setDark(!isDark), you should implement it getting the previous state setDark(state => !state) since setting the state is deferred until re-render.
Working Stackblitz
By the way, <ThemeProvider theme={isDark ? darkTheme : lightTheme}>, is that line a typo? If you are trying to split the Context in two (value and dispatch, which it is a nice idea), I would do it as follows:
const ThemeContext = createContext({
isDark: false
});
export const useTheme = () => useContext(ThemeContext);
export default ThemeContext;
const ToggleThemeContext = createContext({
toggleTheme: () => console.warn('Still using initial value')
});
export const useToggleTheme = () => useContext(ToggleThemeContext);
export default ToggleThemeContext;
//CustomThemeProvider.tsx
export const CustomThemeProvider = ({ children }) => {
const [isDark, setDark] = useState(false);
const memoToggleTheme = useCallback(() => setDark(state => !state), [
setDark
]);
return (
<ToggleThemeContext.Provider value={memoToggleTheme}>
<ThemeContext.Provider value={isDark}>{children}</ThemeContext.Provider>
</ToggleThemeContext.Provider>
);
};
Working Stackblitz memoizing the component which dispatches the action because otherwise it will be re-rendered by the App component when the theme changes.
By doing that only the component that uses the value will be re-rendered.
Let me link you an article I wrote the last day about everything related to React Context, including optimization React Context, All in One

Navigation v5 React Native

I have some question on React Native Navigations v5
I have a sub function in my main function. I also have screenOption = (navData) function for customise my navigation and have few header button here which out of my Main function. My Problem now i am not sure how to access the subFunction that located in my mainfunction from my screenOption = (navData). For example whenclick the icon on the header Button and it will trigger the dispatch function to store which located inside the main function.
const mainfunction = (props) =>{
subfunction = async(data) =>{
dispatch(action.delete(data)
}
return ()
}
export const screenOptions = (navData) => {
Item
title="Delete"
iconName={
Platform.OS === "android" ? "md-create" : "ios-trash-outline"
}
onPress={() => {
trigger subfunction here
}}
/>
}
Try creating a custom button and add the component using setOptions
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => <CustomHeaderButton onPress={customOnPress} />
});
}, []);

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 Navigation, Redux, How to change screens?

My routes are not triggering screen changes using the latest React Navigation v1.5.0.
I have integrated Redux in my setup as detailed in the react-navigation-redux docs. The biggest change I see here is the 'addListener' setup, although I'm not sure this is what is preventing the screen changes. My routes were working fine using v.1.0.
I see the navigation action being fired and the screen being added to the navigation state in the debugger, but the screen isn't changing.
Clicking on the button below dispatches the action, but the screen doesn't change to the About screen and stays on the Home screen.
RootNav
StackNavigator
- Home
- About
Index.js
const middleware = createReactNavigationReduxMiddleware(
"root",
state => state.navigationState,
)
const addListener = createReduxBoundAddListener("root");
class App extends Component {
render () {
return (
<View>
<RootNav navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.navigationState,
addListener,
})} />
</View>
)
}
}
BUTTON
<TouchableHighlight onPress={ () =>
this.props.dispatch(NavigationActions.navigate({
routeName: 'About'
})) }>
<Text>About</Text>
</TouchableHighlight>
STATE AFTER CLICKING BUTTON:
nav: {
key: StackRouterRoot,
index: 1,
isTransitioning: true,
routes: [
0: {routeName: 'Home'},
1: {routeName: 'About'},
],
}
How do you properly dispatch route/screen changes?
Your navigation state is not bound to the redux, considering that you're using redux-navigation. You need to create a Navigation reducer and bind it to the store.
For eg
// Nav.js
// This is your exported router, which gets the initial State
import AppNavigator from '../Navigation/AppNavigation'
const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('LaunchScreen'))
export default reducer = (state = initialState, action) => {
const newState = AppNavigator.router.getStateForAction(action, state)
return newState || state
}
and bind it to your store like this
// Store.js
const RootReducer = combineReducers(config, {
nav: Nav,
// ...other reducers
});
and finally use it in your Redux Navigation state as
function ReduxNavigation (props) {
const addListener = createReduxBoundAddListener('root')
const { dispatch, nav } = props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav, // this is bound to redux store using mapStateToProps
addListener
})
return <AppNavigation navigation={navigation} />
}
const mapStateToProps = state => ({ nav: state.nav })
export default connect(mapStateToProps)(ReduxNavigation)
as mentioned in the docs