Start React root render after Server fetch success - reactjs

I want to fetch some data (like translations/initial settings etc.) and after that launch an application
How do I should do that in the best way?
Now I am rendering a spinner and after the fetch success I re-render React root.
But I don't sure that is really good way.
Thanks for any help !
//launch fetch and wait for the response. After that re -render Root
bootPreparationInit()
.then(() => {
render(
<RHTLContainer>
<MuiThemeProvider>
<RootContainer store={store} history={history} />
</MuiThemeProvider>
</RHTLContainer>,
document.getElementById("root")
);
});
// it for tests. Because Karma sometimes can't see the root element
(() => {
if (!document.getElementById("root")) {
const rootEl = document.createElement("div");
rootEl.setAttribute("id", "root");
document.body.appendChild(rootEl);
}
})();
// render a spinner before data load
render(
<RHTLContainer>
<MuiThemeProvider>
<div className="spinner-ico-box">
<CircularProgress />
</div>
</MuiThemeProvider>
</RHTLContainer>,
document.getElementById("root")
);
// it for webpack HMR
if (module.hot) {
module.hot.accept("./core/containers/Root.container", () => {
const NewRootContainer = require("./core/containers/Root.container").default;
render(
<RHTLContainer>
<NewRootContainer store={store} history={history} />
</RHTLContainer>,
document.getElementById("root")
);
});
}

I'd suggest fetching data in RHTLContainer component constructor and on fetch success saving fetched data in state:
constructor() {
...
this.state = {
dataLoading: true;
};
bootPreparationInit()
.then((responseData) => {
...
this.setState({
dataLoading: false,
fetchedData: respondeData
});
});
}
And then inside your component you can use this.state.dataLoading to conditionally show spinner.

Related

ReactJS Testing - Check if component did render after async call in componentDidMount

Sometimes my react application crashes on some pages, after I changed the API. Now I want to add tests to the application that check, if the components (re)render without crashing after an API call has been made (all calls are made inside the componentDidMount() function).
Currently my test is really simple and looks like this:
it(route.path + ' renders without crashing', async () => {
const div = document.createElement('div');
const Component = route.component;
ReactDOM.render(
<Provider store={store}>
<Component />
</Provider>
, div);
});
But this only checks, if the application renders, before the API call returned something.
How can I add something like an await, to check if the data from componentDidMountloaded?
Thanks for your help.
EDIT
I checked out RTL - but I'm still having trouble. It always says that it can't find the element. Here is my code:
import { render, waitFor } from '#testing-library/react'
import '#testing-library/jest-dom/extend-expect'
test('examples of some things', async () => {
const { getByTestId, findByTestId } = render(
<Provider store={store}>
<Page />
</Provider>
);
await waitFor(() =>
expect(getByTestId("table")).toBeInTheDocument()
);
})
EDIT 2
The <Page /> component renders a <AnotherComponent />, Inside this component, an Ajax call is made to load some data, which then should be rendered:
export default class Page {
render(
...
...
<AnotherComponent />
);
}
export default class AnotherComponent {
componentDidMount() {
  someApiCall().then(set State...);
}
render(
....
...
{
this.state.someAPIdata &&
<div data-testid="table">
...
...
</div>
}
);
}
When I run the test, the tests already returns an error, even before the API call finished

Testing navigation in React component

I'd like to test that the url changes, when a submit button is pressed. As part of the test, I'm checking that the initial url is "/auth" and the url becomes "/".
A simpler test is failing, though, with the initial url test.
Test:
it("displays an authcode and submit button", async() => {
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<MemoryRouter initialEntries={["/auth"]}>
<App />
</MemoryRouter>,
root
);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(screen.getByLabelText('Auth code:')).toBeVisible();
expect(screen.getByRole('button')).toBeVisible();
expect(location.pathname).toBe("/auth");
});
App component:
import React from "react";
import { Route } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute';
import { CreateProfileWithRouter } from './CreateProfileComponent';
import { ActivityList } from './ActivityListComponent';
import { TokenEntryWithRouter } from './TokenEntryComponent';
export class App extends React.Component {
render() {
return (
<div>
<ProtectedRoute exact path="/" component={ActivityList} />
<Route path="/login" component={CreateProfileWithRouter} />
<Route path="/auth" component={TokenEntryWithRouter} />
</div>
);
}
}
Result:
expect(received).toBe(expected) // Object.is equality
Expected: "/auth"
Received: "/"
After some more trial and error, I figured something out. "/" is the initial url, but I don't know how to change that. I'm passing the url that the component will navigate to and asserting that "/" is the url, at the beginning, and, when navigation is tested, I assert the url has changed to the passed in url.
I'm also using Router instead of MemoryRouter. I had a hunch from the docs that the history prop, which is passed into the component (with "withRouter"), gets changed in a way that could be tested.
Before all tests:
beforeEach(() => {
jest.resetAllMocks();
createPermanentAuthSpy = jest.spyOn(yasClient, "createPermanentAuth");
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<Router history={history}>
<TokenEntryWithRouter navigateToOnAuthentication="/dummy" />
</Router>,
root
);
token = screen.getByLabelText('Auth code:');
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/");
});
Testing navigation:
it("navigates to '/', when a good token is entered.", async() => {
createPermanentAuthSpy.mockImplementationOnce(() => Promise.resolve(true));
await act(async() => {
fireEvent.change(token, { target: { value: '1' } });
fireEvent.submit(screen.getByTestId('create-permanent-auth'));
});
expect(createPermanentAuthSpy).toHaveBeenCalledTimes(1);
expect(token.classList.contains('valid-data')).toBe(true);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/dummy");
});

Material ui does not style the pages in my isomorphic react app

I have followed all the guide in setting up Material ui in isomorphic environment but my pages are still white without design showing on the text.
I am using universal-react-redux-starter-kit.
In firefox, I ge this in the console
Unknown property ‘mui-prepared’. Declaration dropped. localhost:3000
Unknown property ‘mui-prepared’. Declaration dropped. localhost:3000
Unknown property ‘mui-prepared’. Declaration dropped.
In Chrome I get this
Uncaught (in promise) TypeError: Request scheme 'chrome-extension' is unsupported
at 80235ae9f69d4d5feb32.serviceworker.js:sourcemap:618
at
And my page looks like this
For days now, I have been unable to figure out the issue.
//AppContainer.js
...
render () {
const { layout, history, routes, routerKey, store, userAgent } = this.props
const muiTheme = getMuiTheme({
palette: {
primary1Color: green500,
primary2Color: green700,
primary3Color: green100,
},
}, {
avatar: {
borderColor: null,
},
userAgent: userAgent,
});
return (
<MuiThemeProvider muiTheme={muiTheme}>
<Provider store={store}>
<div style={{ height: '100%' }}>
<Helmet {...Object.assign(clone(defaultLayout), layout)} />
<Router history={history} children={routes} key={routerKey} />
</div>
</Provider>
</MuiThemeProvider>
)
}
...
client.js sample
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
// ========================================================
// Browser History Setup
// ========================================================
const browserHistory = useRouterHistory(createBrowserHistory)({
basename: __BASENAME__
})
// ========================================================
// Store and History Instantiation
// ========================================================
// Create redux store and sync with react-router-redux. We have installed
the
// react-router-redux reducer under the routerKey "router" in
src/routes/index.js,
// so we need to provide a custom `selectLocationState` to inform
// react-router-redux of its location.
const initialState = window.___INITIAL_STATE__
const store = createStore(initialState, browserHistory)
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState: (state) => state.router
})
// ========================================================
// Developer Tools Setup
// ========================================================
if (0 && __DEBUG__) {
if (window.devToolsExtension) {
window.devToolsExtension.open()
}
}
// ========================================================
// Render Setup
// ========================================================
const MOUNT_NODE = document.getElementById('root')
let render = (routerKey = null) => {
const routes = require('./routes/index').default(store)
match({ history, routes }, (error, redirectLocation, renderProps) => {
// todo: Error handling should be improved
if (error) {
console.log(error)
return
}
Resolver.render(
() => <AppContainer
{...renderProps}
store={store}
history={history}
routes={routes}
routerKey={routerKey}
userAgent={global.navigator.userAgent}
layout={{...layout, ...(window.___LAYOUT__ || {})}}
/>,
MOUNT_NODE
)
})
}
I would try to guess you are using create-react-app as a boilerplate, by looking at the port number in you picture. You are probably not using MaterialUI correctly.
This answer could help.

React-router universal. Why Root component does not re-render?

I have universal app (server rendering).
My routes.js looks like this
routes.js
<Route path="/" component={Root}>
<Route path="sales/:id" component={View} />
...
</Route>
--
Root.js
class Root extends Component {
...
render() {
return (
<div>
<Header {...someProps} />
{this.props.children}
</div>
);
}
}
--
server.js
app.use((req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
...
} else if (redirectLocation) {
...
} else if (renderProps) {
const initialState = {};
const store = configureStore(initialState);
fetchComponentData(store.dispatch, renderProps.components, renderProps.params).then(() => {
const html = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
res.status(200).send(renderFullPage({ html, initialState: store.getState()}));
});
} else {
...
}
});
});
My Root and View components connected to store (redux).
It works well without server rendering and initial rendering on server works good. Problem begins when I am trying to change the state and expect Root component get new props and re-render the tree, but in my case Root component is not re-renders and View component re-renders.
I can't understand how can it be? Why part of my tree is just static html?
UPDATE:
I have not found answer yet, but I assume that Root just not giving new state. If so why Root component is not connected to the store?

(Universal React + redux + react-router) How to avoid re-fetching route data on initial browser load?

I am using a static fetchData method on my Route component...
const mapStateToProps = (state) => ({
posts: state.posts
})
#connect(mapStateToProps)
class Blog extends Component {
static fetchData (dispatch) {
return dispatch(fetchPosts())
}
render () {
return (
<PostsList posts={this.props.posts} />
)
}
}
... and collecting all promises before the initial render on the server side...
match({ routes, location }, (error, redirectLocation, renderProps) => {
const promises = renderProps.components
.filter((component) => component.fetchData)
.map((component) => component.fetchData(store.dispatch))
Promise.all(promises).then(() => {
res.status(200).send(renderView())
})
})
It works fine, the server waits until all my promises are resolved before rendering app.
Now, on my client script, I am doing something similar as on the server...
...
function resolveRoute (props) {
props.components
.filter((component) => component.fetchData)
.map((component) => component.fetchData(store.dispatch))
return <RouterContext {...props} />
}
render((
<Provider store={store}>
<Router
history={browserHistory}
routes={routes}
render={resolveRoute} />
</Provider>
), document.querySelector('#app'))
And it works fine. But, as you may deduct, on the initial page render, the static fetchData is getting called twice (once on the server and once on the client), and I don't want that.
Is there any suggestions on how to solve this? Recommendations?
I'm typing this from my phone, so I apologize for the lack of formatting.
For my project, I'm doing something similar to you; I have a static fetchData method, I loop through the components from renderProps and then I call the static method and wait for the promises to resolve.
I then, call get state from my redux store, stringify it, and pass it to my render function on the server so that it can render out an initial state object on the client.
From the client, I just grab that inital state variable and pass it to my redux store. Redux will then handle getting your client store to match the one on the server. From there, you just pass your store to the provider and go on as usual. You shouldn't need to call your static method on the client at all.
For an example of what I said, you can check out my github project as code explains itself. https://github.com/mr-antivirus/riur
Hope that helped!
[Edit] Here is the code!
Client.js
'use strict'
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import createStore from '../shared/store/createStore';
import routes from '../shared/routes';
const store = createStore(window.__app_data);
const history = browserHistory;
render (
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>,
document.getElementById('content')
)
Server.js
app.use((req, res, next) => {
match({ routes, location:req.url }, (err, redirectLocation, renderProps) => {
if (err) {
return res.status(500).send(err);
}
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
if (!renderProps) {
return next();
}
// Create the redux store.
const store = createStore();
// Retrieve the promises from React Router components that have a fetchData method.
// We use this data to populate our store for server side rendering.
const fetchedData = renderProps.components
.filter(component => component.fetchData)
.map(component => component.fetchData(store, renderProps.params));
// Wait until ALL promises are successful before rendering.
Promise.all(fetchedData)
.then(() => {
const asset = {
javascript: {
main: '/js/bundle.js'
}
};
const appContent = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
)
const isProd = process.env.NODE_ENV !== 'production' ? false : true;
res.send('<!doctype html>' + renderToStaticMarkup(<Html assets={asset} content={appContent} store={store} isProd={isProd} />));
})
.catch((err) => {
// TODO: Perform better error logging.
console.log(err);
});
});
});
RedditContainer.js
class Reddit extends Component {
// Used by the server, ONLY, to fetch data
static fetchData(store) {
const { selectedSubreddit } = store.getState();
return store.dispatch(fetchPosts(selectedSubreddit));
}
// This will be called once on the client
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props;
dispatch(fetchPostsIfNeeded(selectedSubreddit));
}
... Other methods
};
HTML.js
'use strict';
import React, { Component, PropTypes } from 'react';
import ReactDom from 'react-dom';
import Helmet from 'react-helmet';
import serialize from 'serialize-javascript';
export default class Layout extends Component {
static propTypes = {
assets: PropTypes.object,
content: PropTypes.string,
store: PropTypes.object,
isProd: PropTypes.bool
}
render () {
const { assets, content, store, isProd } = this.props;
const head = Helmet.rewind();
const attrs = head.htmlAttributes.toComponent();
return (
<html {...attrs}>
<head>
{head.base.toComponent()}
{head.title.toComponent()}
{head.meta.toComponent()}
{head.link.toComponent()}
{head.script.toComponent()}
<link rel='shortcut icon' href='/favicon.ico' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
</head>
<body>
<div id='content' dangerouslySetInnerHTML={{__html: content}} />
<script dangerouslySetInnerHTML={{__html: `window.__app_data=${serialize(store.getState())}; window.__isProduction=${isProd}`}} charSet='utf-8' />
<script src={assets.javascript.main} charSet='utf-8' />
</body>
</html>
);
}
};
To reiterate...
On the client, grab the state variable and pass it to your store.
On the server, loop through your components calling fetchData and passing your store. Wait for the promises to be resolved, then render.
In HTML.js (Your renderView function), serialize your Redux store and render the output to a javascript variable for the client.
In your React component, create a static fetchData method for ONLY the server to call. Dispatch the actions you need.
You can use CanUseDOM from fbjs module.
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
//only render on the server because it doesn't have DOM
if(!canUseDOM)
static fetch here
A better idea is to dehydrate your store state on the server side and hydrate the initial store state on the client side with the dehydrated state.
From Redux Doc:
This makes it easy to create universal apps, as the state from your server can be serialized and hydrated into the client with no extra coding effort.
http://redux.js.org/docs/introduction/ThreePrinciples.html

Resources