Initial browser re-rendering when fetching data asynchronously - reactjs

I'm trying to build a React app/site that uses "react-router", "redux" and server-side rendering and I don't quite understand how to deal with the initial rendering in the browser.
On the server side, I can take a JSON object, pass it to the state and then map state to props - all synchronously - and finally render the component using already initialized props.
const routes = <Router>
<Route path='/' component={ViewComposite}
onEnter={ ({params}) => { store.dispatch(setViewProps(props)); }} />
<Route path='/admin' component={AdminComposite}
onEnter={ ({params}) => { store.dispatch(setAdminProps(props)); }} />
</Router>;
However, when the browser comes into play, I need to fetch data via AJAX request - asynchronously:
window.onload = function() {
const store = createStore(viewPropsReducer);
ReactDOM.render(
<Provider store={store} >
<Router history={browserHistory} >
<Route path="/" component={ViewComposite}
onEnter={ ({params}) => {store.dispatch(fetchViewData()); }} />
<Route path='/admin' component={AdminComposite}
onEnter={ ({params}) => {store.dispatch(fetchAdminData()); }} />
</Router>
</Provider>,
document.getElementById('viewMount')
);}
As a result, the rendering fails due to undefined props.
Is there a way to work around this - maybe postpone browser rendering until the data is fetched?

You could add the json payload to the server rendered html.
<script>
window.payload = <json>
</script>
If your state is serializable, I guess you could just put the state there instead, and pass it as initialState when creating the store.
createStore(reducer, window.initialState);

Related

React router route loader not working on nested components

Am using react router v6 and i would like to use the new loader to load the data before the component loads. So i have the following
In my index.js
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="*"
loader={async ({ params }) => {
console.log("index loader log"); //this logs
return true;
}}
element={<App />}
> </Route>
)
);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<RouterProvider router={router} />
);
in my app component i have nested routes like
const App = () => {
return (
<>
<Routes>
<Route path="auth/*" element={<AuthLayout/>}/>
<Route path="about"
loader={async ({ params }) => {
console.log("about child loader log"); //this doesnt log
return true;
}}
element={<AboutPage/>}/>
</Routes>
<h1>Testing app</h1>
</>
);
}
On the app component the loader on the Route path="about" does not console.log when i visit the about route but the component is rendered. What am i missing for the loader to work on the child route.
Based on some basic testing it seems that in order for the new RRDv6.4 data APIs to work you need to specify the complete routing configuration in the createBrowserRouter function.
There does however appear to already be an issue filed with #remix-run/react-router for this behavior as a reported bug, so you may want to follow it if it ever addressed/resolved. (I suspect it was you since the name is "geoffrey" and the timing is coincidentally about an hour ago around the same time as this post)
This above issue has since been closed with comment:
Descendant <Routes> trees do not participate in data loading
(https://reactrouter.com/en/main/components/routes) since they cannot
be known ahead of render-time. You'll need to lift your descendant
route definitions up into the routes you pass to createBrowserRouter.
The relevant information regarding the descendent routes and the new Data API can be found in the Routes documentation in a note.
Note:
If you're using a data router like createBrowserRouter it is
uncommon to use this component as it does not participate in data
loading.
Hoist the entire route declaration to the parent creating the data router. The following does work with the loader function for the "/about" route and About component.
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="*"
loader={({ params }) => {
console.log("index loader log");
return "This is the App";
}}
element={<App />}
>
<Route path="auth/*" element={<AuthLayout />} />
<Route
path="about"
loader={({ params }) => {
console.log("about child loader log");
return "this is the about page";
}}
element={<AboutPage />}
/>
</Route>
)
);
The App component should render an Outlet for the nested routes to render their content into.
import { Outlet } from 'react-router-dom';
const App = () => {
return (
<>
<h1>Testing app</h1>
<Outlet />
</>
);
};

React detecting location change to force refresh page (To update useEffect when people use browsers built in back/forward buttons)

Currently have a problem where when my users use their browsers built in forward/back buttons it doesn't reload the state of my page. (Due to the states being set and rendered in a useEffect)
To fix this I have tried to monitor when the location changes (eg, they go to another route) when this happens I force reload the page.
However, this results in an infinite loop of refreshing. I'm not sure how to counter this?
I am on react router v6.3
export const App = () => {
let location = useLocation();
useEffect(() => {
window.location.reload();
}, [location]);
return (
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route exact path="/example" element={<Callback />} />
</Routes>
);
};
export const Router = () => {
return (
<BrowserRouter>
<App />
</BrowserRouter>
);
};

How do I pass state to components when using a `react-router` config

I'm using a route config as defined here. But, when using this method of router configuration how do I pass props to the components?
Looking at the following method from the docs there doesn't seem to be a way to provide props:
const RouteWithSubRoutes = (route) => (
<Route path={route.path} render={props => (
// pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes}/>
)}/>
)
Doing something like the following doesn't work. The prop doesn't get passed through:
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} user={this.state.user} {...route}/>
))}
I am using React's default state management. Do I need Redux for this?
Here is example of how to use Flux + React together to pass props. I hope this is useful for you. Let me know if you have other questions.
AppComponent.js
In your AppComponent, you would need to pass only the pages you need to render.
render () {
<div>
<Route path="/login" component={Login} />
<Route exact path= "/:date?" component={Dashboard} />
</div>
}
In your app.js component, you import all services, actions, and main store component you need. Checkout bottle.js for easier way to pass values and services. However, you would need to just render
ReactDOM.render(
(
<BrowserRouter>
<Route path="/" AppStore={AppStore} render={props => <AppComponent {...props} AppStore={AppStore} />} />
</BrowserRouter>
),
document.getElementById("view-container")
);
You should let the parent component like the component Dashboard in AppComponment.js get the data passed from app.js to AppComponent to Dashboard (and Dashboard's children components).
As for AppStore, this would be like a container for all your other stores. You would need import all your other data store components and actions.

React Redux Router Component Has Empty State

I am writing an react website that has a master-detail feel to it. I make server calls to populate my master component. When a user clicks an item on the master component, it uses React Router to navigate to a detail view. However, my state is no longer populated. Instead it appears I'm getting my initial state in this new component.
As far as I know, I'm using the latest on all the libraries.
index.js
const store = storeFactory( initialState );
const myHistory = createBrowserHistory();
ReactDOM.render(<Provider store={store}>
<Router history={ myHistory }>
<div>
<Route exact path='/' component={App} />
<Route exact path='/contrib' component={Contributions} />
<Route exact path='/story/:id' component={FullStory} />
</div>
</Router>
</Provider>,
document.getElementById('root'));
Story.jsx (a child of App)
<div className="faux-row">
<Link className="invisible-link" to={`/story/${this.props.story.id}`}>
<div className="story">
<div className="story-header">
FullStory.jsx
const mapStateToProps = (state) => {
console.log(state);
return {
stories: state.story_groups
}
}
story_groups as shown here is empty. How do I keep the state?
EDIT:
I am using redux-sage for middleware to call the server. The dispatch is coming out of ComponentWillMount() in the App component.

React - Component Will Mount not getting called when changing routes?

When changing from indexRoute to the Route with path ':topic', the page just stays the same.
My routes look like this:
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path='/' addHandlerKey={true} component={App}>
<IndexRoute component={ArticleList} />
<Route path='/:topic' component={ArticleList} />
<Route path='/:id' component={Article} />
</Route>
</Router>
</Provider>, document.getElementById('app'));
ArticleList looks like this:
const _ArticleList = React.createClass({
componentWillMount () {
this.props.fetchArticles();
},
render () {
return (
<div id='ArticleList' className='box'>
{this.props.articles.map(function (article, i) {
return <ArticleCard id={article._id} key={i} title={article.title} votes={article.votes} />;
})}
</div>
);
}
When I navigate to the route then refresh the page, it changes to what I want, just not when navigating to it directly.
To be clear, the url is changing (thanks to App component) so params.topic is changing but the component is not re-rendering so componentWillMount is not getting triggered. (Have tried componentWillReceiveProps but this didnt work)
The problem you are running into is that once the index page has loaded the component will have mounted and wont unmount unless you change to a completely different route all together. You are just updating a child route which doesn't retrigger the componentWillMount to fire off again.
You need to revisit componentWillReceiveProps because that would be the right way to do this.
something like:
componentWillReceiveProps(nextProps) {
// in our own app we have acces to routeParams as the :id portion not sure if that works on your app
// but idea should be if the topic has changed then refetch the articles
if (this.props.routeParams !== nextProps.routeParams) {
this.props.fetchArticles();
}
}
I noticed the same behavior when I had attempted to use conponentWillMount() -- which is a misspelling of componentWillMount().
The misspelling didn't trigger any warnings or errors, and of course didn't trigger the events I intended to call.

Resources