I'm trying to turn a block of hardcoded <Route />s into something that's dynamically generated from a config variable. Eg.
From
<Router>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/about" component={About} />
<Route path="/documents" component={Documents} exact />
<Route path="/documents/faq" component={DocFAQ} />
<Route path="/documents/translations" component={Translations} />
</Switch>
</Router>
To
const routes = [
{
path: '/',
component: Home,
exact: true
},
{
path: '/about',
component: About
},
{
path: '/documents',
component: Documents,
children: [
{
path: '/faq',
component: DocFAQ
},
{
path: '/translations',
component: Translations
}
]
}
];
const RecursiveRoute = ({ route, parentPath = '' }) => {
const path = parentPath + route.path;
return (
<React.Fragment>
<Route path={path} exact={route.exact || (route.children != null)} component={route.component} />
{route.children && route.children.map((childRoute, i) => <RecursiveRoute key={i} route={childRoute} parentPath={path} />)}
</React.Fragment>
);
};
const Routes = () => (
<Router>
<Switch>
{routes.map((route, i) => <RecursiveRoute key={i} route={route} />)}
</Switch>
</Router>
);
This code generates exactly what I want when I take the mapping call outside of <Router>; eg. I can verify that it outputs the exact same code as the hardcoded block from before. When it's inside the <Switch> however, only the first route in the routes array is getting mapped--nothing else is generated. Putting logging statements inside <RecursiveRoute> confirms this.
Why is this and how do I fix it?
Another weird thing is that if I paste <RecursiveRoute>'s JSX right into the map statement, it works (except I can't make it recursive in this case):
<Switch>
{routes.map((route, i) => <Route key={i} path={route.path} exact={route.exact || (route.children != null)} component={route.component} />)}
</Switch>
But if I keep it outsourced to another component, the mapping fails again.
[Edit] Solution:
Based on mehamasum's answer, changing <RecursiveRoute> from a component to a function solved this problem:
function generateRecursiveRoute(route, parentPath = '') {
const path = parentPath + route.path;
const routeHasChildren = (route.children != null);
const baseHtml = <Route path={path} exact={route.exact || routeHasChildren} component={route.component} key={path} />;
return !routeHasChildren ?
baseHtml :
<React.Fragment key={path}>
{baseHtml}
{route.children.map((childRoute) => generateRecursiveRoute(childRoute, path))}
</React.Fragment>;
}
const Routes = () => (
<Router>
<Switch>
{routes.map((route) => generateRecursiveRoute(route))}
</Switch>
</Router>
);
Switch is implemented in such a way that you provide multiple Components as children and it will render the first Component's component with matching path. So if you provide something other than a Route as child (something that doesn't have those path and component props) you need to make sure you write your rendering logic in there. Because Switch doesn't know what to do here, as it sees no component or render prop associated with that child.
So when you were mapping to Routes directly they were working, but when you were mapping to RecursiveRoute it was failing. It had no path nor component on it, right?
I have updated your code a little bit so that the routes are directly rendered as children of Switch:
// refactor of RecursiveRoute to
// use it as an utility function, not as component
const getRecursiveRoute = route => {
if (route.children) {
const RootComponent = route.component;
return (
<Route
key={route.path}
path={route.path}
render={props => {
return (
<React.Fragment>
{/* render the root matching component*/}
<RootComponent {...props} />
{/* render the subroutes */}
{/* TODO: consider having a Switch here too */}
{route.children.map(route =>
getRecursiveRoute({
...route,
path: props.match.url + route.path
})
)}
</React.Fragment>
);
}}
/>
);
}
return (
<Route
key={route.path}
path={route.path}
exact={route.exact || route.children != null}
component={route.component}
/>
);
};
const Routes = () => {
return (
<Router>
<Switch>{routes.map(route => getRecursiveRoute(route))}</Switch>
</Router>
);
};
You might have to fix some edge cases about exact routing. Hope it helps.
Related
I am having an issue where it seems my state variable is being overwritten by the default/initial value I set when calling useState(). I have const [token, setToken] = useState(""). I pass in the setToken function to my Login component, and when I log in I update the token via the setToken function.
In my App.js I only want to render the Login component if the token is not set, otherwise, I redirect to a Post.js component. Once I log in my token is being set and I am being redirected to my Posts.js page, the problem is if I refresh the page the token is overwritten again to the initial value and I'm not sure why that is.
My understanding is that useState() is only called on the initial render, otherwise the state is obtained from whatever is stored in state which since I called the setToken function, should be the state with the token, not an empty string.
What am I missing?
App.js
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState("");
console.log('token', token)
return (
<Fragment>
<Router>
<Navbar />
<Route exact path="/" component={Landing} />
<Container>
{!errors.length ? <></> : <Alerts errors={errors} handleError={setErrors}/> }
<Switch>
<Route
exact path="/login"
render={ token ? (props) => <Redirect push to="/posts" {...props} /> : (props) => (<Login error={setErrors} isLoggedIn={setToken} {...props} />)}
/>
<Route exact path="/posts"
render={token ? (props) => <Posts token={token} {...props} /> : (props) => <Redirect push to="/login" {...props} />} />
</Switch>
</Container>
</Router>
</Fragment>
);
}
Pertinent section of Landing.js
const handleSubmit = async (e) => {
e.preventDefault();
const res = await login(email, password);
if(res.status !== 200){
props.error(res.data.errors);
}else{
props.error([])
props.isLoggedIn(res.data.token)
}
}
UPDATE:
I did figure out how to do what I wanted to do, by storing my token in local storage. In my App.js I'm doing this now
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState(null);
useEffect(() => {
setToken(window.localStorage.getItem("token"));
})
This is not overwriting my token state as it was initially, so it's doing what I want, but I'm still wondering what I was doing wrong in my original implementation?
when you refresh the page your app runs from the beginning. If you don't want to lose your token on page refresh you should save/retrieve your to/from localStorage.
I built a simple reddit app with React-Redux and when I go to a post page like /posts/mlxft5 and refresh the page it says that the post is not found. I can't figure out how to fix it. Here's my code on codesandbox.
here's my app.js code
function App() {
return (
<Router >
<div className="App">
<div className="App-navbar">
<SubredditsList />
</div>
<Switch>
<Route exact path="/" component={StartMessage}/>
<Route exact path="/search" component={SearchPostsList}/>
<Route exact path="/posts" component={PostsList} />
<Route path="/posts/:postId" component={PostDetailRender}/>
<Route path="/search/:postId" component={SingleSearchPostRender}/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
export default App`
Right now you are only requesting posts from the API when we are on the /posts page for a subreddit. There are no requests initiated by /posts/mlxft. We need to add an additional thunk action that can fetch and store a single post from the id.
We don't want to fetch posts that are already in Redux when we click on a single post from the list on the /posts page. We we will use the condition setting of the createAsyncThunk function to conditionally cancel the fetch if the data already exists.
export const fetchSinglePost = createAsyncThunk(
"posts/fetchSingle",
async (postId) => {
const response = await fetch(
`https://api.reddit.com/api/info/?id=t3_${postId}`
);
const json = await response.json();
return json.data.children[0].data;
},
{
condition: (postId, { getState }) => {
const { posts } = getState();
if (posts.entities[postId]) {
return false;
}
}
}
);
You need to add additional cases in your reducer to handle this thunk. Note: if you use builder callback notation instead of reducer map object notation then you could combine your two "rejected" cases.
[fetchSinglePost.pending]: (state, action) => {
state.status = "loading";
},
[fetchSinglePost.fulfilled]: (state, action) => {
state.status = "succeeded";
postsAdapter.upsertOne(state, action.payload);
},
[fetchSinglePost.rejected]: (state, action) => {
state.status = "failed";
state.error = action.error.message;
}
Inside of your PostDetailRender component you need to dispatch the fetchSinglePost action. It's ok to dispatch it all cases because the thunk itself will cancel the fetching.
useEffect(() => {
dispatch(fetchSinglePost(postId));
}, [dispatch, postId]);
You could potentially have a status for each post rather than one for the whole slice. I explain how to do that in this answer.
Updated CodeSanbox
I also made some changes so that you don't fetch the same subreddit's posts more than once.
I'm loading translations dynamically, with the HttpBackend
However inside my component I use the Trans component, and it is screaming about missing translation key, I also see the init finished after it tried to access the Trans component.
I have a Suspens around my app, why is this happens?
I get in the console:
i18next::translator: missingKey en translation myKey This is text that has a in it....
the init happens after.
How can I fix this?
// file: i18n.js
i18n
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en',
debug: true,
supportedLngs: ['en', 'bg', 'rs'],
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
backend: {
loadPath: '/static/{{lng}}.translations.json',
},
react: {
wait: true,
useSuspense: true,
},
transSupportBasicHtmlNodes: true,
});
export default i18n;
// app.js
function App() {
return (
<BrowserRouter>
<Apollo>
<Suspense fallback={<Loading />}>
<ThemeProvider theme={theme}>
<Header />
<Routes />
<Footer />
</ThemeProvider>
</Suspense>
</Apollo>
</BrowserRouter>
);
}
problematic component:
//home
const I18N_TEXT_KEY_CONSTANT = 'text_key';
const Home = () => (
<Trans i18nKey={I18N_TEXT_KEY_CONSTANT}>
This is text that has a <br /> in it and also some random spaces.
</Trans>
);
You should pass the t function to Trans component.
//home
import { useTranslation } from 'react-i18next';
const I18N_TEXT_KEY_CONSTANT = 'text_key';
const Home = () => {
const { t } = useTranslation();
return (
<Trans i18nKey={I18N_TEXT_KEY_CONSTANT} t={t}>
This is text that has a <br /> in it and also some random spaces.
</Trans>
);
};
The solution which is sadly not document is:
const {t} = useTranstions()
<Trans i18nKey="someKey" t={t}>
Passing the t into the Trans works perfectly.
Recently tried adding redux to one of the apps. One of the solutions is to have root component wrapped within Provider. Did this, but still seeing below issue (in the browser). [Pasted only potentially relevant code from files].
Uncaught Error: Could not find "store" in either the context or props
of "Connect(Gallery)". Either wrap the root component in a ,
or explicitly pass "store" as a prop to "Connect(Gallery)".
client/main.js
document.addEventListener('DOMContentLoaded', function() {
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('mount')
);
});
shared/App./js
class App extends React.Component {
render() {
return (
<BrowserRouter history={ browserHistory }>
<div>
<Route exact path="/" component={Gallery} />
<Route path="/viewitem/:id" component={ViewItem} />
</div>
</BrowserRouter>
);
}
}
shared/redux/index.js
export const reducers = combineReducers({
images: imageReducer,
});
export function configureStore(initialState = {}) {
const store = createStore(
reducers,
initialState,
applyMiddleware(...middleWare)
)
return store;
};
export const store = configureStore();
Could it be that BrowserRouter might not work with redux?
I'm trying to use the example of translating your own component
so i'm doing this in app.js:
const messages = {
en: {
myroot: {
hello: {
world: 'Hello, World!',
},
},
},
};
const App = () => (
<Admin message={messages} locale="en" ...>
<Resource name="myresource" edit={EditPage} />
and in my Translation component:
import React from 'react';
import { translate } from 'admin-on-rest';
const Translation = ({ translate }) => (
<button>{translate('myroot.hello.world')}</button>
);
export default translate(Translation);
finally in my EditPage:
import Translation from 'path/to/Translation';
export const EditPage = (props) => (
<Edit {...props}>
<Translation />
</Edit>
);
its not working for me. its just showing myroot.hello.world in the button.
could you please help me out with that?
Typo ?
<Admin messages={messages} locale="en" ...>
Note that the prop is messages and not message