Why React batch not working in async callback - reactjs

I'm using React "version": "18.2.0" which must support batching everywhere(in callback, in setTimeout and in Promises).
I have redux store which looks like this:
const initialState = {
token: null,
};
const accessSlice = createSlice({
name: 'access',
initialState,
reducers: {
setToken: (state, action) => {
state.token = action.payload;
},
},
});
I have react component which renders route(from react-router v5) only if we do not have token in redux store and if we do not have token this component redirects us to root page:
import { Redirect, Route } from 'react-router-dom';
import { tokenSelector } from '../redux/slices/access';
const UnauthorizedRoute = ({ exact, path, children }) => {
// we get token from redux store
const token = useSelector(tokenSelector);
return (
<Route
exact={exact}
path={path}
>
{token ? children : <Redirect to="/" />}
</Route>
};
};
Then we have LoginPage component which we render like this:
<UnauthorizedRoute exact path="/login">
<LoginPage />
</UnauthorizedRoute>
In LoginPage component on form submit I have callback which looks like this:
const handleOnSubmit = async (formValues) => {
const token = await backendApi.getToken(formValues);
dispatch(setToken(token));
history.push('/welcome-page');
}
<LoginForm
onSubmit={handleOnSubmit}
/>
I expect that react will rerender application only once after these code executes
dispatch(setToken(token));
history.push('/welcome-page');
In other words I expect that react will batch these two function calls(first call dispatches redux action and second call redirects us to welcome page). But in reality after executing this code user is redirected to root page '/' because it looks like after executing these dispatch and history.push UnauthorizedRoute component gets new token from redux store and redirects us to root page '/'.
But if change handleSubmit callback like this(we do not go to backend and use fake token):
const handleOnSubmit = async (formValues) => {
dispatch(setToken('faketoken'));
history.push('/welcome-page');
}
<LoginForm
onSubmit={handleOnSubmit}
/>
all works! user is redirected to '/welcome-page' and in redux store token value is 'faketoken'
Maybe somebody knows how to fix it? I tried to wrap these code using unstable_batchedUpdates but it does not help
unstable_batchedUpdates(() => {
dispatch(setToken(token));
history.push('/welcome-page');
});

Not sure why but this code fixed the issue:
import { flushSync } from 'react-dom';
flushSync(() => {
dispatch(setToken(token));
history.push('/welcome-page');
});

Related

Navigate to different page on successful login

App.tsx
import React from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch
} from 'react-router-dom';
import './app.css';
import Login from './auth/pages/login';
import DashBoard from './dashboard/dashboard';
export const App = () => {
return (
<div className="app">
<Router>
<Switch>
<Route path="/auth/login" component={Login} />
<Route path="/dashboard" component={DashBoard} />
<Redirect from="/" exact to="/auth/login" />
</Switch>
</Router>
</div>
);
};
export default App;
login.tsx
import { useHistory } from 'react-router-dom';
const authHandler = async (email, password) => {
const history = useHistory();
try {
const authService = new AuthService();
await authService
.login({
email,
password
})
.then(() => {
history.push('/dashboard');
});
} catch (err) {
console.log(err);
}
};
From the above code I'm trying to navigate to dashboard on successful login.
The auth handler function is being called once the submit button is clicked.
The login details are successfully got in authhandler function, but once I use history to navigate I get the following error
"Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component"
Error text is pretty clear. You can not call useHistory, or any other hook, outside of functional component. Also, hooks must be called unconditionally, on top of component. Try to call useHistory inside your actual component and pass history as a parameter to authHandler.
The problem is that authHandler is an async function and using a hook inside a "normal" function don't work. It breaks the rule of hooks.
What you need to do is separate authHandler and history.push('/dashboard').
What you can do is return the async request and use .then to call history.push.
const authHandler = async (email, password) => {
const authService = new AuthService();
// returning the request
return await authService
.login({
email,
password
})
};
And inside your component you use the useHistory hook and call authHandler on some action.
const MyComponent = () => {
const history = useHistory()
const onClick = (email, password) => {
authHandler(email, password)
.then(() => history.push('/dashboard'))
}
return (...)
}

How to properly ask for props on component

I have a component SinglePost that is called when another component Post is clicked in the main page, and also through path /post/:id. The thing is, in SinglePost, I call an API through Redux on componentDidMount, and then ask for it on componentWillReceiveProps. And when you access the component from the URL /post/:id, the props throws undefined two times after it loads.
I was following a React/Redux tutorial, to make a CRUD web app with API calls. And, in that project, the props loads fine in both cases (through main page navigation, and from URL). I did exactly the same thing and it doesn't work. Is this a problem, or does it work this way?
This is the SinglePost component:
import { connect } from 'react-redux';
import { mostrarPublicacion } from '../../actions/publicacionesActions';
state = {
publicacion: {},
}
componentDidMount() {
const {id} = this.props.match.params;
//this is the action defined on PublicacionesActions
this.props.mostrarPublicacion(id);
}
componentWillReceiveProps(nextProps, nextState) {
const {publicacion} = nextProps;
this.setState({
publicacion
})
}
const mapStateToProps = state => ({
publicacion: state.publicaciones.publicacion
})
export default connect(mapStateToProps, {mostrarPublicacion}) (SinglePost);
PublicacionesActions:
export const mostrarPublicacion = (id) => async dispatch => {
const respuesta = await axios.get(`http://www.someapi.com:8000/api/publicacion/${id}`);
dispatch({
type: MOSTRAR_PUBLICACION,
payload: respuesta.data
})
}
I debugged this and it actually returns the publication. In fact, it renders properly in the SinglePost component. But at first it loads undefined when accessing the component through the url.
RootReducer:
import { combineReducers } from 'redux';
import publicacionesReducer from './publicacionesReducer';
import seccionesReducer from './seccionesReducer';
export default combineReducers({
publicaciones: publicacionesReducer,
secciones: seccionesReducer
});
PublicacionesReducer:
export default function (state = initialState, action) {
switch(action.type) {
case MOSTRAR_PUBLICACION:
return {
...state,
publicacion: action.payload
}
default:
return state
}
}
This is the Router component (wrapped into <Provider store={store}>):
<Switch>
<Route exact path='/post/:id' component={SinglePost} />
</Switch>
I actually ask for (!this.state.publication) and return null, to render the component. It is working, but it is necesary?

redux-react-route v4/v5 push() is not working inside thunk action

In index.js push directly or throw dispatch works well:
...
import { push } from 'react-router-redux'
const browserHistory = createBrowserHistory()
export const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware, routerMiddleware(browserHistory))
)
// in v5 this line is deprecated
export const history = syncHistoryWithStore(browserHistory, store)
history.push('/any') // works well
store.dispatch(push('/any')) // works well
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>
), document.getElementById('root'))
App.js
class App extends Component {
render() {
return (
<div className="app">
<Switch>
<Route path="/" component={Main} />
<Route path="/any" component={Any} />
</Switch>
</div>
);
}
}
export default withRouter(connect(/*...*/)(App))
but in redux-thunk action all attempts ends by rewriting url, but without re-rendering
...
export function myAction(){
return (dispatch) => {
// fetch something and then I want to redirect...
history.push('/any') // change url but not re-render
dispatch(push('/any')) // change url but not re-render
store.dispatch(push('/any')) // change url but not re-render
}
}
This myAction is calling fetch() inside and should redirect after success.
If I run this.props.history.push('/any') inside component, it works! but I need to run redirect inside thunk action after successful fetch()
I was trying wrap all components with withRouter or Route, but didn't help.
Inject history object into your component and use push like this:
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
#withRouter
#connect(({auth})=>({auth}))
class App extends Component {
// on redux state change
componentWillReceiveProps(nextProps) {
if(!nextProps.auth)
this.props.history.push('/login')
}
render() {
return (
<div>
<Button
// on button click
onClick={this.props.history.push('/')}
>
Home page
</Button>
</div>
);
}
}
I made workaround by delegating state of successfull fetch() to the component (thanks #oklas) where is history.push() or <Redirect> working:
{this.props.fetchSuccessfull && <Redirect to="/any" />}
But still waiting for better solution by calling push() directly from thunk action.
Well, let me then submit another not perfect solution by passing the history object in the dispatch to the action. I guess it's more a beginners-solution but is IMHO simple to understand (and therefore simple to maintain which is the most important thing in software-development)
Using <BrowserRouter> makes all React-compoments having the history in their props. Very convenient. But, as the problem description stated, you want it outside a React Component, like an action on Redux-Thunk.
Instead of going back to <Router> I chose to stick to BrowserRouter.
The history object cannot be accessed outside React Components
I did not like going back to <Router> and using something like react-router-redux
Only option left is to pass along the history object to the action.
In a Auth-ForgotPassword component:
const submitHandler = (data) => {
dispatch(authActions.forgotpassword({data, history:props.history}));
}
In the action function
export const forgotpassword = ({forgotpasswordData, history}) => {
return async dispatch => {
const url = settings.api.hostname + 'auth/forgotpassword'; // Go to the API
const responseData = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(forgotpasswordData),
}
);
history.push('/auth/forgotpassword/success');
}
}
And now we all wait for the final elegant solution :-)

Redux/Firebase - Component not updating after uid is received in state

I'm using Firebase's authentication API for my Redux app. I have an app component that is supposed to be informed when a user's UID authentication is received and toggle which header is shown to the user (either logged in header or logged out header). However, in instances where the user is already logged in and revisiting the main route, I can't seem to get my main app to re-render when the UID is stored to the app state.
An outline of the flow is such:
In routes/index.js: Firebase's onAuthStateChanged observer
identifies if the user is already logged in or not when the main
route loads. If there's a user, dispatch actions to copy their UID
from Firebase to our state and send them to the "connect" page.
In actions.jsx: The startLoginForAuthorizedUser action creator
dispatches an action to update the auth reducer with the new UID &
reroute the user to "connect".
In reducers.jsx: The "auth" state is updated to include the user's
UID to allow components to toggle elements depending on
authentication status.
In App.jsx: For whatever reason, mapStateToProps is not receiving an
updated state, even though the user is authenticated and Redux dev
tools shows the state as updated with the new UID.
The end result is that authenticated users see the "connect" page as expected, but still see a logged-out header. Here's the code:
App.jsx
import React from 'react';
import { connect } from 'react-redux';
import * as actions from 'actions';
import HeaderLoggedOut from './HeaderLoggedOut';
import ModalOverlay from './ModalOverlay';
import MenuWrapper from './MenuWrapper';
import Header from './Header';
import Tabs from './Tabs';
export const App = React.createClass({
render(){
const { uid } = this.props;
console.log("App.jsx: uid:", uid);
if(!uid){
return(
<div>
<HeaderLoggedOut />
<div className="tonal-main">
<div className="tonal-content">
{ this.props.children }
</div>
</div>
<ModalOverlay />
</div>
);
}
return(
<MenuWrapper>
<Header />
<div className="tonal-content">
{ this.props.children }
</div>
<Tabs />
</MenuWrapper>
);
}
});
const mapStateToProps = (state) => {
// state is not updated
return {
uid: state.auth.uid
};
};
export default connect(mapStateToProps)(App);
router/index.js
import App from 'App.jsx';
import Connect from 'connect/Connect.jsx';
import firebase from 'app/firebase';
const store = require('store').configure();
firebase.auth().onAuthStateChanged((user) => {
if(user.emailVerified && user.uid){
store.dispatch(actions.startLoginForAuthorizedUser(user.uid));
} else {
browserHistory.push('/');
}
});
const requireLogin = (nextState, replace, next) => {
const currentUser = firebase.auth().currentUser;
if(!currentUser){
replace('/');
}
next();
};
const redirectIfLoggedIn = (nextState, replace, next) => {
const currentUser = firebase.auth.currentUser;
if(currentUser){
replace('/connect');
}
next();
};
export default (
<Router history={ browserHistory }>
<Route path="/" component={ App }>
<IndexRoute component={ Landing } onEnter={ redirectIfLoggedIn } />
<Route path="connect" component = { Connect } onEnter = { requireLogin } />
</Route>
</Router>
);
Actions.jsx
// ...actions, imports, etc...
export var startLoginForAuthorizedUser = (uid) => {
return (dispatch) => {
dispatch(login(uid));
dispatch(pushToRoute('/connect'));
};
};
export var login = (uid) => {
console.log('actions: logging in user with uid ', uid);
return{
type: 'LOGIN',
uid
};
};
export var pushToRoute = (route) => {
browserHistory.push(route);
};
Reducer.jsx
const authInitialState = {
uid: ""
};
export const authReducer = (state = authInitialState, action) => {
switch(action.type){
case 'LOGIN':
return {
...state,
uid: action.uid
};
case 'LOGOUT':
return {
...state,
uid: ""
};
default:
return state;
}
};
Any help from you experts would be greatly appreciated. This is driving me nuts.
I figured it out after an embarrassing amount of trial and error. For anyone else in the future who wants to use both react-router/react-router-dom and firebase's onAuthStateChanged() observer, just know you have to keep the observer code at your topmost component (where you render your provider in ReactDOM.render()).
If you keep it anywhere else, it doesn't appear to work properly and I frankly have no idea why. If someone more familiar with firebase wanted to explain what was happening there and why it would work in the top-level component but not in the routes index file, I'm sure it would help someone down the road.

React/Redux - dispatch action on app load/init

I have token authentication from a server, so when my Redux app is loaded initially I need make a request to this server to check whether user is authenticated or not, and if yes I should get token.
I have found that using Redux core INIT actions is not recommended, so how can I dispatch an action, before app is rendered?
You can dispatch an action in Root componentDidMount method and in render method you can verify auth status.
Something like this:
class App extends Component {
componentDidMount() {
this.props.getAuth()
}
render() {
return this.props.isReady
? <div> ready </div>
: <div>not ready</div>
}
}
const mapStateToProps = (state) => ({
isReady: state.isReady,
})
const mapDispatchToProps = {
getAuth,
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
All of the answers here seem to be variations on creating a root component and firing it in the componentDidMount. One of the things I enjoy most about redux is that it decouples data fetching from component lifecycles. I see no reason why it should be any different in this case.
If you are importing your store into the root index.js file, you can just dispatch your action creator(let's call it initScript()) in that file and it will fire before anything gets loaded.
For example:
//index.js
store.dispatch(initScript());
ReactDOM.render(
<Provider store={store}>
<Routes />
</Provider>,
document.getElementById('root')
);
I've not been happy with any solutions that have been put forward for this, and then it occurred to me that I was thinking about classes needing to be rendered. What about if I just created a class for startup and then push things into the componentDidMount method and just have the render display a loading screen?
<Provider store={store}>
<Startup>
<Router>
<Switch>
<Route exact path='/' component={Homepage} />
</Switch>
</Router>
</Startup>
</Provider>
And then have something like this:
class Startup extends Component {
static propTypes = {
connection: PropTypes.object
}
componentDidMount() {
this.props.actions.initialiseConnection();
}
render() {
return this.props.connection
? this.props.children
: (<p>Loading...</p>);
}
}
function mapStateToProps(state) {
return {
connection: state.connection
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Startup);
Then write some redux actions to async initialise your app. Works a treat.
If you are using React Hooks, one single-line solution is
useEffect(() => store.dispatch(handleAppInit()), []);
The empty array ensures it is called only once, on the first render.
Full example:
import React, { useEffect } from 'react';
import { Provider } from 'react-redux';
import AppInitActions from './store/actions/appInit';
import store from './store';
export default function App() {
useEffect(() => store.dispatch(AppInitActions.handleAppInit()), []);
return (
<Provider store={store}>
<div>
Hello World
</div>
</Provider>
);
}
Update 2020:
Alongside with other solutions, I am using Redux middleware to check each request for failed login attempts:
export default () => next => action => {
const result = next(action);
const { type, payload } = result;
if (type.endsWith('Failure')) {
if (payload.status === 401) {
removeToken();
window.location.replace('/login');
}
}
return result;
};
Update 2018: This answer is for React Router 3
I solved this problem using react-router onEnter props. This is how code looks like:
// this function is called only once, before application initially starts to render react-route and any of its related DOM elements
// it can be used to add init config settings to the application
function onAppInit(dispatch) {
return (nextState, replace, callback) => {
dispatch(performTokenRequest())
.then(() => {
// callback is like a "next" function, app initialization is stopped until it is called.
callback();
});
};
}
const App = () => (
<Provider store={store}>
<IntlProvider locale={language} messages={messages}>
<div>
<Router history={history}>
<Route path="/" component={MainLayout} onEnter={onAppInit(store.dispatch)}>
<IndexRoute component={HomePage} />
<Route path="about" component={AboutPage} />
</Route>
</Router>
</div>
</IntlProvider>
</Provider>
);
With the redux-saga middleware you can do it nicely.
Just define a saga which is not watching for dispatched action (e.g. with take or takeLatest) before being triggered. When forked from the root saga like that it will run exactly once at startup of the app.
The following is an incomplete example which requires a bit of knowledge about the redux-saga package but illustrates the point:
sagas/launchSaga.js
import { call, put } from 'redux-saga/effects';
import { launchStart, launchComplete } from '../actions/launch';
import { authenticationSuccess } from '../actions/authentication';
import { getAuthData } from '../utils/authentication';
// ... imports of other actions/functions etc..
/**
* Place for initial configurations to run once when the app starts.
*/
const launchSaga = function* launchSaga() {
yield put(launchStart());
// Your authentication handling can go here.
const authData = yield call(getAuthData, { params: ... });
// ... some more authentication logic
yield put(authenticationSuccess(authData)); // dispatch an action to notify the redux store of your authentication result
yield put(launchComplete());
};
export default [launchSaga];
The code above dispatches a launchStart and launchComplete redux action which you should create. It is a good practice to create such actions as they come in handy to notify the state to do other stuff whenever the launch started or completed.
Your root saga should then fork this launchSaga saga:
sagas/index.js
import { fork, all } from 'redux-saga/effects';
import launchSaga from './launchSaga';
// ... other saga imports
// Single entry point to start all sagas at once
const root = function* rootSaga() {
yield all([
fork( ... )
// ... other sagas
fork(launchSaga)
]);
};
export default root;
Please read the really good documentation of redux-saga for more information about it.
Here's an answer using the latest in React (16.8), Hooks:
import { appPreInit } from '../store/actions';
// app preInit is an action: const appPreInit = () => ({ type: APP_PRE_INIT })
import { useDispatch } from 'react-redux';
export default App() {
const dispatch = useDispatch();
// only change the dispatch effect when dispatch has changed, which should be never
useEffect(() => dispatch(appPreInit()), [ dispatch ]);
return (<div>---your app here---</div>);
}
I was using redux-thunk to fetch Accounts under a user from an API end-point on app init, and it was async so data was coming in after my app rendered and most of the solutions above did not do wonders for me and some are depreciated. So I looked to componentDidUpdate(). So basically on APP init I had to have accounts lists from API, and my redux store accounts would be null or []. Resorted to this after.
class SwitchAccount extends Component {
constructor(props) {
super(props);
this.Format_Account_List = this.Format_Account_List.bind(this); //function to format list for html form drop down
//Local state
this.state = {
formattedUserAccounts : [], //Accounts list with html formatting for drop down
selectedUserAccount: [] //selected account by user
}
}
//Check if accounts has been updated by redux thunk and update state
componentDidUpdate(prevProps) {
if (prevProps.accounts !== this.props.accounts) {
this.Format_Account_List(this.props.accounts);
}
}
//take the JSON data and work with it :-)
Format_Account_List(json_data){
let a_users_list = []; //create user array
for(let i = 0; i < json_data.length; i++) {
let data = JSON.parse(json_data[i]);
let s_username = <option key={i} value={data.s_username}>{data.s_username}</option>;
a_users_list.push(s_username); //object
}
this.setState({formattedUserAccounts: a_users_list}); //state for drop down list (html formatted)
}
changeAccount() {
//do some account change checks here
}
render() {
return (
<Form >
<Form.Group >
<Form.Control onChange={e => this.setState( {selectedUserAccount : e.target.value})} as="select">
{this.state.formattedUserAccounts}
</Form.Control>
</Form.Group>
<Button variant="info" size="lg" onClick={this.changeAccount} block>Select</Button>
</Form>
);
}
}
const mapStateToProps = state => ({
accounts: state.accountSelection.accounts, //accounts from redux store
});
export default connect(mapStateToProps)(SwitchAccount);
If you're using React Hooks, you can simply dispatch an action by using React.useEffect
React.useEffect(props.dispatchOnAuthListener, []);
I use this pattern for register onAuthStateChanged listener
function App(props) {
const [user, setUser] = React.useState(props.authUser);
React.useEffect(() => setUser(props.authUser), [props.authUser]);
React.useEffect(props.dispatchOnAuthListener, []);
return <>{user.loading ? "Loading.." :"Hello! User"}<>;
}
const mapStateToProps = (state) => {
return {
authUser: state.authentication,
};
};
const mapDispatchToProps = (dispatch) => {
return {
dispatchOnAuthListener: () => dispatch(registerOnAuthListener()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Same solution as Chris Kemp mentions above. Could be even more generic, just a canLift func not tied to redux?
interface Props {
selector: (state: RootState) => boolean;
loader?: JSX.Element;
}
const ReduxGate: React.FC<Props> = (props) => {
const canLiftGate = useAppSelector(props.selector);
return canLiftGate ? <>{props.children}</> : props.loader || <Loading />;
};
export default ReduxGate;
Using: Apollo Client 2.0, React-Router v4, React 16 (Fiber)
The answer selected use old React Router v3. I needed to do 'dispatch' to load global settings for the app. The trick is using componentWillUpdate, although the example is using apollo client, and not fetch the solutions is equivalent.
You don't need boucle of
SettingsLoad.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from "redux";
import {
graphql,
compose,
} from 'react-apollo';
import {appSettingsLoad} from './actions/appActions';
import defQls from './defQls';
import {resolvePathObj} from "./utils/helper";
class SettingsLoad extends Component {
constructor(props) {
super(props);
}
componentWillMount() { // this give infinite loop or no sense if componente will mount or not, because render is called a lot of times
}
//componentWillReceiveProps(newProps) { // this give infinite loop
componentWillUpdate(newProps) {
const newrecord = resolvePathObj(newProps, 'getOrgSettings.getOrgSettings.record');
const oldrecord = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
if (newrecord === oldrecord) {
// when oldrecord (undefined) !== newrecord (string), means ql is loaded, and this will happens
// one time, rest of time:
// oldrecord (undefined) == newrecord (undefined) // nothing loaded
// oldrecord (string) == newrecord (string) // ql loaded and present in props
return false;
}
if (typeof newrecord ==='undefined') {
return false;
}
// here will executed one time
setTimeout(() => {
this.props.appSettingsLoad( JSON.parse(this.props.getOrgSettings.getOrgSettings.record));
}, 1000);
}
componentDidMount() {
//console.log('did mount this props', this.props);
}
render() {
const record = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
return record
? this.props.children
: (<p>...</p>);
}
}
const withGraphql = compose(
graphql(defQls.loadTable, {
name: 'loadTable',
options: props => {
const optionsValues = { };
optionsValues.fetchPolicy = 'network-only';
return optionsValues ;
},
}),
)(SettingsLoad);
const mapStateToProps = (state, ownProps) => {
return {
myState: state,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators ({appSettingsLoad, dispatch }, dispatch ); // to set this.props.dispatch
};
const ComponentFull = connect(
mapStateToProps ,
mapDispatchToProps,
)(withGraphql);
export default ComponentFull;
App.js
class App extends Component<Props> {
render() {
return (
<ApolloProvider client={client}>
<Provider store={store} >
<SettingsLoad>
<BrowserRouter>
<Switch>
<LayoutContainer
t={t}
i18n={i18n}
path="/myaccount"
component={MyAccount}
title="form.myAccount"
/>
<LayoutContainer
t={t}
i18n={i18n}
path="/dashboard"
component={Dashboard}
title="menu.dashboard"
/>

Resources