I18Next - wait for Redux store to be set with local data - react-native

I would like for i18next to wait for the redux store to be ready. I'm storing the user's chosen language in the store, using persistor from redux-persist to rehydrate it at app startup. I tried to set the language from the store :
// ...
import store from '../redux';
// ...
const lng = store.getState().user.language
? store.getState().user.language
: Localization.locale.slice(0, 2);
i18next
// .use(languageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
resources,
lng,
});
But at this point the store is still in its initialState and not rehydrated yet. Is there a way I could do this?

So the PersistGate can implement an onBeforeLift method, waiting for it to be resolved before lifting the "loading" state.
const onBeforeLift = async () => {
await initI18Next();
};
export default function App(): React.ReactElement {
return (
<Provider store={store}>
<PersistGate
loading={<SplashScreen />}
persistor={persistor}
onBeforeLift={onBeforeLift}
>
<StatusBar hidden />
<MainNavigation />
</PersistGate>
</Provider>
);
}
And in i18n.ts:
const resources = {
en: { translation: en },
fr: { translation: fr },
he: { translation: he },
};
export const init = () => {
const lng = store.getState().user.language
? store.getState().user.language
: Localization.locale.slice(0, 2);
return (
i18next
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
resources,
lng,
})
);
};
export default i18next;

Related

Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a... or pass a custom React context provider

Check multitude of questioned already asked and but still can't figure this one out.
We are rewriting our authentication layer using
export default AuthContext = React.createContext();
and wrapping it around our AppNavigator
function AppNavigator(props) {
const [state, dispatch] = useReducer(accountReducer, INITIAL_STATE);
const authContext = React.useMemo(
() => ({
loadUser: async () => {
const token = await keychainStorage.getItem("token");
if (token) {
await dispatch({ type: SIGN_IN_SUCCESS, token: token });
}
},
signIn: async (data) => {
client
.post(LOGIN_CUSTOMER_RESOURCE, data)
.then((res) => {
const token = res.data.accessToken;
keychainStorage.setItem("token", token);
dispatch({ type: SIGN_IN_SUCCESS, token: token });
})
.catch((x) => {
dispatch({ type: SIGN_IN_FAIL });
});
},
signOut: () => {
client.delete({
LOGOUT_CUSTOMER_RESOURCE
});
dispatch({ type: SIGN_OUT_SUCCESS });
}
}),
[]
);
console.log("token start", state.token);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer
theme={MyTheme}
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
onStateChange={(state) => {
NavigationService.setAnalytics(state);
}}
>
<AppStack.Navigator initialRouteName="App" screenOptions={hideHeader}>
{state.token != null ? (
<AppStack.Screen name="App" component={AuthMainTabNavigator} />
) : (
<>
<AppStack.Screen name="App" component={MainTabNavigator} />
<AppStack.Screen name="Auth" component={AuthNavigator} />
</>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
export default AppNavigator;
App.js - render fucnction
<Root>
<StoreProvider store={store} context={AuthContext}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<AppNavigator context={AuthContext}/>
</SafeAreaProvider>
</PersistGate>
</StoreProvider>
</Root>
HomeScreen.js
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
But still receiving
Error: Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(HomeScreen) in connect options.
We have gone through the REDUX documentation:
https://react-redux.js.org/using-react-redux/accessing-store#using-the-usestore-hook
Simply can not work out why we are receiving this error.
I'm not sure what you're trying to accomplish here, but this is very wrong:
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
It looks like you're mixing up two different things. You're trying to create a context for use with your own auth state, but you're also trying to use that same context instance to override React-Redux's own default context instance. Don't do that! You should not be passing a custom context instance to connect and <Provider> except in very rare situations.
I understand what you are trying to achieve only after reading through your discussion in the comments with #markerikson.
The example from the React Navigation docs creates a context AuthContext in order to make the auth functions available to its descendants. It needs to do this because the state and the dispatch come from the React.useReducer hook so they only exist within the scope of the component.
Your setup is different because you are using Redux. Your state and dispatch are already available to your component through the React-Redux context Provider and can be accessed with connect, useSelector, and useDispatch. You do not need an additional context to store your auth info.
You can work with the context that you already have using custom hooks. Instead of using const { signIn } = React.useContext(AuthContext) like in the example, you can create a setup where you would use const { signIn } = useAuth(). Your useAuth hook can access your Redux store by using the React-Redux hooks internally.
Here's what that code looks like as a hook:
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';
import { useDispatch } from "react-redux";
export const useAuth = () => {
// access dispatch from react-redux
const dispatch = useDispatch();
React.useEffect(() => {
// same as in example
}, []);
// this is the same as the example too
const authContext = useMemo(
() => ({
signIn: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
// but instead of passing that `authContext` object to a `Provider`, just return it!
return authContext;
}
In your component, which must be inside your React-Redux <Provider>:
function App() {
const { signIn } = useAuth();
const [username, setUsername] = React.useState('');
return (
<Button onPress={() => signIn(username)}>Sign In</Button>
)
}

React Native - I18Next with react-navigation and typescript

I have this RN project I started. I need localization in it, and I tried numerous solution for it. I18Next looks like it could really well suit my needs. I'm not sure how to define it in its own file and call it in App.tsx. I used a useEffect but I doubt it's wise - even more without any dependencies. Here is what I tried :
i18n.ts:
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import en from './en.json';
import fr from './fr.json';
const resources = {en: { translation: en }, fr: { translation: fr }};
i18next
// .use(languageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
resources,
lng: Localization.locale.slice(0, 2),
});
export default i18next;
And the code in App.tsx:
// ...
import i18next from './src/locales/i18n';
export default function App(): React.ReactElement {
useEffect(() => {
i18next
.init()
.then(() => {
// TODO - dispatch to redux when this is ready
})
.catch(error => console.warn(error));
});
return (
<Provider store={store}>
<PersistGate loading={<SplashScreen />} persistor={persistor}>
<StatusBar hidden />
<MainNavigation />
</PersistGate>
</Provider>
);
}
I get a warning i18next: init: i18next is already initialized. You should call init just once!. Indeed, I'm calling .init() twice - in the main file and again in the useEffect. But I don't see how to do it otherwise.
Also, is my useEffect alright?
[EDIT] I found https://react.i18next.com/latest/i18nextprovider in the doc and using it and deleting the useEffect, the warning is gone - although I'm not sure if it's a good idea since the docs states You will only need to use the provider in scenarios for SSR (ServerSideRendering) or if you need to support multiple i18next instances - eg. if you provide a component library.
Actually I just needed to init it once indeed, and didn't need to use the provider either. Finally nailed it like this:
const onBeforeLift = async () => {
await initI18Next();
};
export default function App(): React.ReactElement {
return (
<Provider store={store}>
<PersistGate
loading={<SplashScreen />}
persistor={persistor}
onBeforeLift={onBeforeLift}
>
<StatusBar hidden />
<MainNavigation />
</PersistGate>
</Provider>
);
}
And in i18n.ts:
const resources = {
en: { translation: en },
fr: { translation: fr },
he: { translation: he },
};
export const init = () => {
const lng = store.getState().user.language
? store.getState().user.language
: Localization.locale.slice(0, 2);
return (
i18next
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
resources,
lng,
})
);
};
export default i18next;

Jest Redux Persist: TypeError: Cannot read property 'catch' of undefined at writeStagedState

I'm trying to test my LoginScreen with Jest and Typescript. I use redux and redux-persist for storage and have set the storage up to use AsyncStorage as part of the config. I suspect that redux-persist is attempting to rehydrate after the built-in time-out function it uses runs out and tries to set storage to default storage? I'm getting the following error:
console.error
redux-persist: rehydrate for "root" called after timeout. undefined
undefined
at _rehydrate (node_modules/redux-persist/lib/persistReducer.js:70:71)
at node_modules/redux-persist/lib/persistReducer.js:102:11
at tryCallOne (node_modules/promise/setimmediate/core.js:37:12)
at Immediate._onImmediate (node_modules/promise/setimmediate/core.js:123:15)
Currently my test looks like this:
describe('Testing LoginScreen', () => {
it('should render correctly', async () => {
const { toJSON } = render(<MockedNavigator component={LoginScreen} />);
await act(async () => await flushMicrotasksQueue());
expect(toJSON()).toMatchSnapshot();
});
});
and my MockNavigator looks like this:
type MockedNavigatorProps = {
component: React.ComponentType<any>;
params?: {};
};
const Stack = createStackNavigator();
const MockedNavigator = (props: MockedNavigatorProps) => {
return (
<MockedStorage>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={props.component}
initialParams={props.params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedStorage>
);
};
export default MockedNavigator;
Here is the way I'm creating my storage:
import 'react-native-gesture-handler';
import * as React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '../src/AppState/store';
type MockedStorageProps = {
children: any;
};
const MockedStorage = (props: MockedStorageProps) => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{props.children}
</PersistGate>
</Provider>
);
};
export default MockedStorage;
I resolved this same error using this advice from an issue on the redux-persist repo: https://github.com/rt2zz/redux-persist/issues/1243#issuecomment-692609748.
(It also had the side-effect of avoiding logging errors in test from redux-logger.)
jest.mock('redux-persist', () => {
const real = jest.requireActual('redux-persist');
return {
...real,
persistReducer: jest
.fn()
.mockImplementation((config, reducers) => reducers),
};
});
#alexbrazier:
It basically just bypasses redux-persist by returning the reducers
directly without wrapping them in redux-persist.

How to pass functions from parent to any children and sub-children using Navigator and WithSecurityScreen in React Native?

I am quite new in React Native and I am trying to understand how function written in a parent Component could be passed (inherited) to any children and sub-children. In particular I am using a library to internationalise my App using:
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
But I noticed that I have to implement the translate(...) function for each Component of the whole project and this seems to be exaggerated because it requires a lot of work to implement the translation feature (I followed this tutorial).
Please, note that I have a basic understanding how to pass a function or some data using this.props, so I am not asking how props works from a parent to a single child. What I am asking is: how to avoid to repeat the code from //BEGIN ... to //END... (please see WithSecurityScreen file) and to avoid to repeat the implementation of handleLocalizationChange, RNLocalize.addEventListener, RNLocalize.removeEventListener and translate.
Please also note that the translation library works, test is provided at following line of WithSecurityScreen:
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}😂</Text></View>;
But I am not be able to pass translate(...) function to each components of the whole project.
The project structure is:
App.js (wraps SecureApp.js)
SecureApp.js (wrapped in App.js and runs WithSecurityScreen.js)
WithSecurityScreen.js (wraps routes to views, e.g. Welcome.js)
Welcome.js (main view)
App.js
import { withSecurityScreen } from './src/components/withSecurityScreen'
import App from "./SecureApp.js"
export default withSecurityScreen(App);
SecureApp.js
const MainNavigator = createStackNavigator({
Home: {
screen: Welcome,
navigationOptions: {
headerShown: false
}
},
UserProfile: {
screen: CoreApp,
navigationOptions: {
headerShown: false
}
},
NumPad: {
screen: NumPad,
navigationOptions: {
header: 'PIN Creation',
headerShown: false
}
}, /* , navigationOptions: {headerLeft: () => null} */
QrScan: {
screen: QrScan, navigationOptions: {
header: 'QR Scan',
headerShown: false
}
},
...
});
export default createAppContainer(MainNavigator);
WithSecurityScreen.js
// START: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
// END: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}😂</Text></View>;
const showSecurityScreenFromAppState = appState =>
['background', 'inactive'].includes(appState);
const withSecurityScreenIOS = Wrapped => {
return class WithSecurityScreen extends React.Component {
constructor(props) {
super(props)
setI18nConfig()
}
state = {
showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState)
};
componentDidMount() {
AppState.addEventListener('change', this.onChangeAppState)
RNLocalize.addEventListener('change', this.handleLocalizationChange)
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onChangeAppState)
RNLocalize.removeEventListener('change', this.handleLocalizationChange)
}
handleLocalizationChange = () => {
setI18nConfig()
.then(() => this.forceUpdate())
.catch(error => {
console.error(error)
})
}
onChangeAppState = nextAppState => {
const showSecurityScreen = showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen})
};
render() {
return this.state.showSecurityScreen
? <SecurityScreen/>
: <Wrapped {...this.props} />
}
}
};
const withSecurityScreenAndroid = Wrapped => Wrapped;
export const withSecurityScreen = Platform.OS === 'ios'
? withSecurityScreenIOS
: withSecurityScreenAndroid;
Welcome.js
export default class Welcome extends Component {
let username = 'UserName';
render() {
return (
<View style={styles.container}>
<LinearGradient colors={globalStyles.colors.gradientGreen} style={{flex: 1}}>
<View style={styles.upperView}><Text style={styles.upperViewText}>{this.props.translate('WELCOME_TEXT')}{this.username}</Text>
</View>
</LinearGradient>
</View>
);
}
}
I get following error:
First of all in your case you can declare translate function in separate js file locale.js and can declare all your translation logic in that file and export the functions translate and setI18nConfig
local.js
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
export const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
export const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
and import this functions in your components where you want to use this like
App.js
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { setI18nConfig, translate } from './locale';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
setI18nConfig(); // This should be called only once through out the app at the time of changing locale.
return (
<View>
<Text> translate ('key_from_json_to_label') </Text>
</View>
);
}
}
For more details you can refer this repo where I have implemented the same.
Hope this will help.
i use import i18n from "i18next", for translation.
Here is the config file:
//config/i18n
import i18n from "i18next";
import { reactI18nextModule, initReactI18next } from "react-i18next";
import translationFR from '../translation/fr/translation.json';
import translationEN from '../translation/en/translation.json';
import DeviceInfo from 'react-native-device-info';
let locale = DeviceInfo.getDeviceLocale().substring(0, 2);
if (locale != 'fr') {
locale = 'en';
}
// the translations
const resources = {
en: translationEN,
fr: translationFR,
};
i18n
.use(reactI18nextModule) // passes i18n down to react-i18next
.init({
resources: resources,
lng: locale,
fallbackLng: ['en', 'fr'],
keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;
One of the uses is to
import i18n from 'config/i18n'
and use translate in file like this
i18n.t('bottomTab:home_tab')
You can also wrap component with withNamespaces from 'react-i18next', like this:
export default withNamespaces([], {wait: true})(Welcome)
and then access translation with:
this.props.t('welcome_message')

Reducer returned undefined during initialization but am returning initialState as default in reducer switch

I am attempting to follow the instructions found in the redux-persist docs to delay the render until rehydration is complete, and I am now receiving this error
"Reducer 'auth' returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined."
However, I am defining and returning the initialState in my auth reducer as follows:
const initialState = {
authenticated: false,
authenticating: false,
loginError: '',
}
export default function reducer(state = initialState, action) {
switch (action.type) {
...my switch cases here
default:
return state
}
}
I did not change how I am creating my store so I am not sure why I am now receiving this error. I will also include my store creation and combine reducers method below
const store = createStore(
reducers,
undefined,
compose(
applyMiddleware(thunk),
autoRehydrate({ log: true }),
window.devToolsExtension ? window.devToolsExtension() : (f) => f
)
)
export default class AppProvider extends Component {
constructor(props) {
super(props)
this.state = {
rehydrated: false,
}
}
componentWillMount() {
persistStore(store, {storage: AsyncStorage}, () => {
this.setState({ rehydrated: true })
})
}
render() {
if (!this.state.rehydrated) {
return <ActivityIndicator />
}
return (
<Provider store={store}>
<App/>
</Provider>
)
}
}
My reducers export file
export default reducers = combineReducers({
auth,
connection,
initialLoad,
queue,
})
It seems to be something on your store creation. The F8App is a good reference for this.
You can do the following:
// configureStore.js
// ... imports
const middlewares = [thunk, promise, array];
var createAppStore = applyMiddleware(...middlewares)(createStore);
export default configureStore = (onComplete) => {
const store = autoRehydrate()(createAppStore)(reducers);
persistStore(store, { storage: AsyncStorage }, onComplete);
return store;
}
// AppProvider.js
// ...
constructor() {
super();
this.state = {
isLoading: true,
store: configureStore(() => {
this.setState({ isLoading: false });
}),
};
}
render() {
if (this.state.isLoading) {
return <ActivityIndicator />;
}
return (
<Provider store={this.state.store}>
<App />
</Provider>
);
}
// ...
The main cause is that you pass in undefined as the preloaded state. Just pass in your root reducer and middlewares and the error should disappear
const store = createStore(
reducers,
compose(
applyMiddleware(thunk),
autoRehydrate({ log: true }),
window.devToolsExtension ? window.devToolsExtension() : (f) => f
)
)
Also, It is complaining that auth is undefined, so you should make sure that all of your reducers are imported correctly in the root reducer reducers.