How to persist data in react native app with usememo hook - javascript

I'm writing an application in react native and I came across a problem - the application will have several screens (I use react-navigation and react-navigation-tabs) and two-color themes (light and dark) managed by context and hooks. What I would like to achieve is the selected theme to be remembered by the app (the light theme will be set as default, and after switching to dark, leaving the application and returning the dark theme should still be applied).
EDIT #2: One answer from yesterday (that disappeared for some reason) suggested the use of redux and local storage so I'm editing the paragraph below to clarify the situation.
Easiest way would be to use sync storage/localStorage (I already have working version of the app using local storage), but one tutorial I found on the web uses the user memo hook for this purpose, and while it should work, it isn't (in my case at least), and I don't know why...
My App.js file below:
imports ...
const TabNavigator = createBottomTabNavigator({
Home: Home,
List: List,
});
const App = createAppContainer(TabNavigator);
export default () => (
<ThemeProvider>
<App />
</ThemeProvider>
ThemeContext.js file:
imports ...
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
And Home.js file, with a button to switch between themes:
imports ...
export const Home = () => {
const { colors, setColors } = useContext(ThemeContext);
const toggleTheme = () => {
if (colors.type === 'light') {
setColors(themes.darkTheme);
} else {
setColors(themes.lightTheme);
}
}
return (
<>
<View style={{...styles.mainView, backgroundColor: colors.backgroundColor }}>
<Text style={{...styles.mainText, color: colors.color}}>Hello Native World</Text>
<Button title='toggle theme' onPress={toggleTheme} />
</View>
</>
)
}
const styles = StyleSheet.create({
mainView: {
paddingTop: 40,
display: 'flex',
alignItems: 'center',
},
mainText: {
fontSize: 40,
fontWeight: 'bold',
},
});

The key file you have to change is your context file:
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
I don't really understand why you use useMemo, but I will leave it. From a quick glance I'd say that it is not needed, but I don't know your app. What you want is something like this:
import AsyncStorage from '#react-community/async-storage'
export const ThemeContext = createContext()
export function usePersistedState(key, initialState) {
const [state, setState] = useState(() => {})
useEffect(() => {
async function getAndSetInitialState() {
const persistedState = await AsyncStorage.getItem(key)
if (persistedState) {
setState(JSON.parse(persistedState))
} else if (typeof initialState === 'function') {
return setState(initialState())
} else {
return setState(initialState)
}
}
getAndSetInitialState()
}, [key])
function setPersistedState(value) {
AsyncStorage.setItem(key, JSON.stringify(value))
setState(value)
}
return [state, setPersistedState]
}
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = usePersistedState("your_storage_key", themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors]
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
I might have missed some edge cases, but this way your app will load it's state from the storage and save it's state into the storage.
EDIT: I'm not sure how useMemo would help, AsyncStorage is the easiest solution imo.

Related

React Native - Type Script: How to save dark mode toggle state even after turning off the application?

What is the correct way so that I can save the dark mode switch even after turning off the application?
I want to use the use-state-persist library to achieve the goal.
In my example I show app.tsx , Preferences.tsx, ViewField.tsx .
So that it will be possible to understand how the logic is built
DarkModeContext
import React from 'react';
interface DarkMode {
isDarkMode: boolean;
setDarkMode: () => void;
}
export const DarkModeContext = React.createContext<DarkMode>({} as DarkMode);
this is the app.tsx
import React, { useEffect, useState } from 'react';
import { syncStorage } from 'use-state-persist';
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const Drawer = createDrawerNavigator();
const initStorage = async () => await syncStorage.init();
const toggleDarkMode = () => {
setDarkMode(!isDarkMode);
};
return (
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
<NavigationContainer>
<Drawer.Navigator
drawerContent={SideMenu}
screenOptions={{
drawerPosition: 'right',
headerShown: false,
drawerType: 'front',
}}
>
<Drawer.Screen name='HomeScreen' component={StackNavigator} />
</Drawer.Navigator>
</NavigationContainer>
</DarkModeContext.Provider>
);
};
export default App;
this is the Preferences.tsx
import React, { useContext, useState } from 'react';
import ViewField from './ViewField';
import { DarkModeContext } from '~/context/DarkModeContext';
const Preferences = () => {
const { isDarkMode, toggleDarkMode } = useContext(DarkModeContext);
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggleDarkMode} />
</View>
);
};
export default Preferences;
this is the ViewField.tsx
import { View, Text, Switch } from 'react-native';
import React from 'react';
import styles from './ViewFieldStyles';
import { useContext } from 'react';
import { DarkModeContext } from '~/context/DarkModeContext';
type Props = {
title: string;
isEnabled: boolean;
setValue: () => void;
};
const ViewField = ({ title, isEnabled, setValue }: Props) => {
const { isDarkMode } = useContext(DarkModeContext);
return (
<View style={isDarkMode ? styles.optionViewDark : styles.optionView}>
<View style={styles.sameRowTextView}>
<Text style={isDarkMode ? styles.optionTextDark : styles.optionText}>{title}</Text>
<View style={styles.switchView}>
<Switch
trackColor={
isDarkMode
? { false: 'rgba(255, 255, 255, 0.38)', true: 'rgba(187, 134, 252, 0.38)' }
: { false: '#767577', true: 'rgba(4, 76, 163, 0.38)' }
}
onValueChange={setValue}
value={isEnabled}
/>
</View>
</View>
</View>
);
};
export default ViewField;
Keep in mind that there seems to be some problems in use-state-persist using Boolean values. Furthermore, the latest published version of this library is from 2020.
However, the use-state-persist library just seems to be a wrapper around AsyncStorage, which is very well maintained. I would encourage you to use this library instead.
In your case, this could be implemented as follows:
Store the actual setter of the state in the context,
Create an effect that accesses the async storage on mount of the application: if there exists a value for the corresponding key, set the state of the context, if not, then do nothing.
In the Preferences component, store a new state in the async storage as well.
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const contextValue = React.useMemo(() => ({
isDarkMode,
setDarkMode
}), [isDarkMode])
React.useEffect(() => {
const load = async () => {
const value = await AsyncStorage.getItem('isDarkMode');
if (value !== null) {
setDarkMode(JSON.parse(value));
}
}
load();
}, [])
return (
<DarkModeContext.Provider value={contextValue}>
...
};
In the Preferences component, set the state and save it to the local storage.
const Preferences = () => {
const { isDarkMode, setDarkMode } = useContext(DarkModeContext);
async function toggle() {
const newValue = JSON.stringify(!isDarkMode);
await AsyncStorage.setItem('isDarkMode', newValue);
setDarkMode(prev => !prev);
}
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggle} />
</View>
);
}

useStyles is not dynamically assigning properties based on variable change

Based on a boolean state change, I'm trying to change the style of a component, but for some reason, it's only displaying the change after a page refresh.
In my parent App component, I do the following:
import React from "react";
import Layout from "./Layout";
export default function App() {
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => {
setLoading(!loading);
}, 2000);
}, [loading]);
return <Layout loading={loading} />;
}
And my Layout component catches this loading variable and send it to the makeStyles hook,
import React from "react";
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles((theme) => ({
root: {},
wrapper: ({ loading }) => ({
paddingTop: 64,
[theme.breakpoints.down("lg")]: {
paddingLeft: loading ? 0 : 100
}
})
}));
const Layout = React.memo(({ loading }) => {
const classes = useStyles({ loading });
return (
<div className={classes.root}>
<div>side</div>
<div className={classes.wrapper}>
is loading: {JSON.stringify(loading)}
</div>
</div>
);
});
export default Layout;
Doing a console.log after wrapper: ({ loading }) => ({ prints the correct value of loading, but the style is not changing.
What's going on here?
there is two issues in your code , the first one is that you use object destructuring two times :
instead of
const Layout = React.memo(({ loading }) => { const classes = useStyles({ loading });
you should :
const Layout = React.memo(({ loading }) => { const classes = useStyles(loading );
because in your first level you have access to the property loading, second issue is that you're invoking the parameter in the wrong place, you must invoke the loading directly in the css property, like this :
paddingLeft: (loading) => (loading ? 0 : 100)
here is a link with the two corrections ,hope that what you are expecting
https://codesandbox.io/s/goofy-microservice-y2gql?fontsize=14&hidenavigation=1&theme=dark

How do I send a function parameter to AsyncStorage?

I want to send the parameter to the function submitLanguageSelection, which is userSelectedLanguage, to a custom hook I've written which (hopefully) saves that parameter to AsyncStorage. The user selects a language, either English or Arabic, from one of the two buttons.
This is my first time ever doing this. I've gotten very stuck.
I would like the submitLanguageSelection function to call the saveData function which is made available through the useLocalStorage hook. I would like the user's choice of language to be persisted in AsyncStorage so I can then later render the ChooseYourLanguageScreen according to whether the user has selected a language or not.
Here is the cutom hook, useLocalStorage:
import React from 'react';
import { Alert } from 'react-native';
import AsyncStorage from '#react-native-community/async-storage';
const STORAGE_KEY = '#has_stored_value';
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async () => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, storedValue);
if (localValue !== null) {
setStoredValue(storedValue);
Alert.alert('Data successfully saved');
}
console.log('stored val', storedValue);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};
Here is the ChooseYourLanguageScreen:
import React from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import useLocalStorage from '../hooks/useLocalStorage';
const ChooseYourLanguageScreen = ({ navigation }) => {
const [saveData, errorMessage] = useLocalStorage();
const submitLanguageSelection = (userSelectedLanguage) => {
//TODO: save the data locally
//TODO: navigate to welcome screen
// at the moment, the language choice isn't making it to useLocalStorage
if (userSelectedLanguage !== null) {
console.log('user selected lang', userSelectedLanguage);
saveData(userSelectedLanguage);
}
};
return (
<View style={styles.container}>
{errorMessage ? <Text>{errorMessage}</Text> : null}
<Text style={styles.text}>This is the Choose Your Language Screen</Text>
<View style={styles.buttons}>
<View>
<Button
title={'English'}
onPress={() => submitLanguageSelection('English')}
/>
</View>
<View>
<Button
title={'Arabic'}
onPress={() => submitLanguageSelection('Arabic')}
/>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
alignSelf: 'center',
},
buttons: {
backgroundColor: '#DDDDDD',
padding: 10,
},
});
export default ChooseYourLanguageScreen;
saveData() needs a parameter. You can provide a default value that uses storedValue that came from React.useState(), but when you call it with an explicit argument it will override that default.
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async (dataToSave = storedValue) => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, dataToSave);
if (localValue !== null) {
setStoredValue(dataToSave);
Alert.alert('Data successfully saved');
}
console.log('stored val', dataToSave);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};

reactjs continuous re rendering

I was watching Wes Bos' speech recognition application, and thought of developing that in React.
So I started and everything was going great, I used hooks and context for state management, but then I got stuck so here what is really happening:
So as the user says any color name that is mentioned, the app strike-through the name of the color (add a css class to that color).
But as soon as another name is called the already striked through color becomes inactive (removes the css class from the previous color name and add the css class to the new one), because that component (ColorList) is getting re-rendered again and again.
I want to persist that strike-through on that individual color name.
Colors component (Which renders all the list of colors)
import React, { useState, useEffect, useContext } from "react";
import { GlobalContext } from "../context/GlobalState";
import { colors } from "../utils/colors";
import { ColorList } from "./ColorList";
const Colors = () => {
const [colors1, setColors] = useState([]);
const colorArray = Object.keys(colors).sort((a, b) => a.length - b.length);
useEffect(() => {
setColors(colorArray);
}, []);
const { color, already } = useContext(GlobalContext);
// console.log("context", context.color);
return (
<div className="colors" style={{ backgroundColor: "green" }}>
{colors1.map((colors, index) => (
<ColorList key={index} colors={colors} />
))}
</div>
);
};
export default Colors;
Color List Component
import React, { useContext } from "react";
import { isDark } from "../utils/colors";
import { GlobalContext } from "../context/GlobalState";
export const ColorList = props => {
const { color} = useContext(GlobalContext);
return (
<span
className={`color ${isDark(props.colors) && "dark"} ${props.colors} ${
props.colors === color ? "got" : ""
}`}
style={{ backgroundColor: `${props.colors}` }}
>
{props.colors}
</span>
);
};
GlobalState Context
import React, { createContext, useReducer } from "react";
import AppReducer from "./AppReducer";
const initialState = {
colorName: "",
alreadyDone: []
};
export const GlobalContext = createContext(initialState);
export const GlobalState = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
function changeColorName(color) {
dispatch({
type: "CHANGE",
payload: color
});
}
function addToAlready(color) {
dispatch({
type: "ADDTO",
payload: color
});
}
console.log("from Context", state);
return (
<GlobalContext.Provider
value={{
color: state.colorName,
changeColor: changeColorName,
alreadyDone: state.alreadyDone,
already: addToAlready
}}
>
{children}
</GlobalContext.Provider>
);
};

React native redux saga TypeError

First of all, I apologise for the long post, I tried to shortened as much as possible.
I am trying to dispatch an action through saga, but I am getting TypeError: resultFunc.apply is not a function error.
The function sets a two variables in the Application reducer:
isFirstTimer: false
appLanguage: en
Store/App/Actions.js
const { Types, Creators } = createActions({
// Set language for the app from user
firstTimer: ['language'],
setDeviceLanguage: ['language'],
})
export const AppTypes = Types
export default Creators
Store/App/Reducers.js
import { INITIAL_STATE } from './InitialState'
import { createReducer } from 'reduxsauce'
import { AppTypes } from './Actions'
export const firstTimer = (state, { language }) => ({
...state,
isFristTimer: false,
appLanguage: language,
})
export const reducer = createReducer(INITIAL_STATE, {
[AppTypes.FIRST_TIMER]: firstTimer,
[AppTypes.SET_DEVICE_LANGUAGE]: setDeviceLanguage,
})
Store/App/Selector.js
import { createSelector } from 'reselect'
const isFristTimer = (state) => state.isFristTimer
const language = (state) => state.language
export const getFirstTimer = createSelector([isFristTimer, language])
Sagas/AppSaga.js
import { put, select } from 'redux-saga/effects'
import * as Selectors from 'App/Stores/App/Selectors'
export function* firstTimer() {
const whereTo = yield select(Selectors.getFirstTimer)
const language = yield select(Selectors.language)
Reactotron.log('getFirstTimer value', whereTo)
Reactotron.log('language value', language)
// When those operations are finished we redirect to the main screen
}
Note: The two variables from the selectors do not log in Reactotron!!.
Sagas/index.js
import { takeLatest, all } from 'redux-saga/effects'
import { firstTimer } from './AppSaga'
import { AppTypes } from 'App/Stores/App/Actions'
export default function* root() {
yield all([
// Call `firstTimer()` when a `FIRST_TIMER` action is triggered
takeLatest(AppTypes.FIRST_TIMER, firstTimer),
])
In my rootScreen.js, I am rendering conditionally according to the mentioned above firstTime variable
componentDidMount() {
Reactotron.log('our state : ' + this.props.app)
}
_setLanguage(language) {
this.props.setFirstTimer(language)
}
render() {
const { isFristTimer } = this.props.app
if (isFristTimer) {
return (
<View style={Helpers.fill}>
<ImageBackground source={Images.firstTimerBg} style={Helpers.fullSize}>
<View
style={[
Style.buttonWrapper,
Helpers.mainCenter,
Helpers.mainSpaceBetween,
Helpers.row,
Metrics.mediumHorizontalMargin,
]}
>
<TouchableOpacity
onPress={() => this._setLanguage('en')}
style={[Style.buttonContainer, Helpers.center]}
>
<Text style={Fonts.normal}>{Words.english}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => this._setLanguage('ar')}
style={[Style.buttonContainer, Helpers.center]}
>
<Text style={Fonts.normal}>{Words.arabic}</Text>
</TouchableOpacity>
</View>
</ImageBackground>
</View>
)
}
return (
<View style={Helpers.fill}>
<AppNavigator
// Initialize the NavigationService (see https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html)
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef)
}}
/>
</View>
)
}
}
The end result :
If I dismiss the error, the application is no longer rendering the first block, and it renders the AppNavigator
The issue is with the getFirstTimer selector - the second argument to createSelector should be the resultFunc (that you see referenced in your error) that takes the results of other selectors it depends on and returns a value. See https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc. Updating getFirstTimer should resolve the issue:
import { createSelector } from 'reselect'
const isFristTimer = (state) => state.isFristTimer
const language = (state) => state.language
export const getFirstTimer = createSelector(
[isFristTimer, language],
// here is where the result func is added to the selector definition
(isFristTimer, language) => ({
isFristTimer,
language,
})
)

Categories

Resources