React Native - I18Next with react-navigation and typescript - react-native

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;

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>
)
}

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

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;

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.

I can't navigate without the navigation prop of react-navigation with react-i18next container

I apply the navigation example without the navigation prop of the react-navigations docs (NavigationService), but I can't make it work with react-i18next.
I applied the example of the documentation https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html in my code:
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/lib/integration/react';
import { createAppContainer } from 'react-navigation';
import { I18nextProvider, withNamespaces } from 'react-i18next';
import { persistor, store } from './src/store';
import I18n from './src/localization';
import Navigation from './src/navigation';
import NavigationService from './src/navigation/NavigationService';
import Loader from './src/screens/components/Loader';
class NavigationStack extends React.Component {
static router = Navigation.router;
render() {
const { t } = this.props;
return <Navigation screenProps={{ t, I18n }} {...this.props} />;
}
};
const ReloadNavOnLanguageChange = withNamespaces(['common', 'server'], {
bindI18n: 'languageChanged',
bindStore: false,
})(createAppContainer(NavigationStack));
export default class App extends React.Component {
...
render() {
return (
<Provider store={store}>
<PersistGate loading={<Loader />} persistor={persistor}>
<I18nextProvider i18n={ I18n } >
<ReloadNavOnLanguageChange ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}} />
</I18nextProvider>
</PersistGate>
</Provider>
);
};
};
// Navigation.js
...
export default Navigation = createSwitchNavigator(
{
AuthLoading: AuthLoadingScreen,
Login: LoginScreen,
App: AppScreen
},
{
initialRouteName: 'AuthLoading'
}
);
// NavigationService.js
Apply the same code that's in https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
// Any JS module of my project (actions, helpers, components, etc.)
import NavigationService from 'path-to-NavigationService.js';
...
NavigationService.navigate('Login');
When the authorization token is validated and the result is negative, the login screen must be opened (NavigationService.navigate('Login')) but it returns the error _navigator.dispatch is not a function in NavigationService.js:
const navigate = (routeName, params) => {
// DISPATCH ERROR
   _navigator.dispatch(
     NavigationActions.navigate({
       routeName,
       params
     })
   );
};
Dependencies:
react 16.5.0
react-native 57.1
react-i18next 9.0.0
react-navigation 3.1.2
Any suggestion? Has anyone else found this scenario?
Using the innerRef option of the withNamespaces hoc of react-i18next instead of passing the function through the ref property of the root component… AS THE DOCUMENTATION OF REACT-I18NETX SAYS!
// App.js
...
const ReloadNavOnLanguageChange = withNamespaces(['common', 'server'], {
bindI18n: 'languageChanged',
bindStore: false,
innerRef: (ref) => NavigationService.setTopLevelNavigator(ref)
})(createAppContainer(NavigationStack));
export default class App extends React.Component {
...
render() {
return (
...
<ReloadNavOnLanguageChange />
...
);
};
}

react-native-navigation theming with styled-components

I'm working with styled-components on my react-native project. We are using react-native-navigation to perform navigation within the application. So the question is how can I implement theme pattern from styled-components in such kind of application?
The problem is that to perform the idea of theming in terms of styled-components I have to wrap my top level component in <ThemeProvider /> like this:
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
But with react-native-navigation I don't have top level component. It has the idea of screens, so the application entry will look like this:
registerScreens(); // this is where you register all of your app's screens
// start the app
Navigation.startSingleScreenApp({
screen: { ... },
drawer: { ... },
passProps: { ... },
...
});
The answer was pretty simple. As react-native-navigation's registerComponent has possibility to pass redux store and Provider as a props:
Navigation.registerComponent('UNIQUE_ID', () => YourComponent, store, Provider);
We can create our custom Provider with both redux and styled-components Providers and pass this custom provider to the registerComponent like this:
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import theme from './theme';
const Provider = ({ store, children }) => (
<Provider store={store}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</Provider>
);
export default Provider;
For more details look #1920.
I think you can do something like this
function wrap(Component) {
return function () {
return (
<ThemeProvider theme={theme}>
<Component />
<AnotherComponent/>
</ThemeProvider>
);
};
}
function registerScreens(store, Provider) {
Navigation.registerComponent('app.SomeScreen', () => wrap(SomeScreen), store, Provider);
// more screens...
}