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?
Related
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");
});
I have this Action
export const UserIsLoggedIn = isLoggedIn => (
{ type: types.USER_IS_LOGGED_IN, isLoggedIn });
this actionConstant
export const USER_IS_LOGGED_IN = "USER_IS_LOGGED_IN";
index.js like this
import { UserIsLoggedIn } from "./redux/actions";
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
this.props.UserIsLoggedIn(true);
}
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
so eventually with above code I get this.props.UserIsLoggedIn is not a function error, if I do UserIsLoggedIn(true); nothing happens... I don't quite understand where the problem is..
within the redux chrome extension I can dispatch with below without an error:
{
type: "USER_IS_LOGGED_IN", isLoggedIn : true
}
below is generally how index looks like
const store = configureStore();
class Index extends Component {
componentWillMount() {
this.getUser();
}
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
console.log(this.props.isUserLoggedIn);
toastr.success("Welcome " + user.profile.given_name);
} else {
this.authService.login();
}
});
};
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
</Provider>
);
}
}
function mapStateToProps(state) {
return {
isUserLoggedIn : state.User.isLoggedIn
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
ReactDOM.render(<Index />, document.getElementById("root"));
serviceWorker.unregister();
Note: Another aspect, mapStateToProps is not working either....
<Provider store={store}> needs to be wrapped around wherever this exported component is used. It can't be within the render method of Index. The way it is now, the connect method won't have access to your store.
You need something like:
ReactDOM.render(<Provider store={store}><Index /></Provider>, document.getElementById("root"));
and then remove the Provider portion from the render method in Index:
render() {
return (
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
);
}
You need to dispatch your action
const mapDispatchToProps = dispatch => ({
UserIsLoggedIn: (value) => {
dispatch(UserIsLoggedIn(value));
}
});
Update:
If you want to use the object syntax you need to wrap that action in a funciton:
const mapDispatchToProps = {
UserIsLoggedIn: (value) => UserIsLoggedIn(value),
};
Thank you #Yusufali2205 and #RyanCogswell for your help but those didn't fix the problem..
For people looking for an answer on this:
index.js is the first file in my React to load then App.js. With this setup, I have <Provider store={store}> inside index.js and inside index.js I don't have an access to the store within render method OR any lifecycles such as willmount or even didmount or willunmount. To access store, create it with <Provider store={store}> inside index.js and access store items within App.js.
About Dispatching action error, I still get Objects are not valid as a React child (found: object with keys {type, isLoggedIn}). If you meant to render a collection of children, use an array instead. error if I try to dispatch within render method with this code {this.props.UserIsLoggedIn(true)}.. I was attempting to do this just to see if dispatching worked, I don't plan to have it actually inside render. When I wrapped it with console.log, it worked fine, like this {console.log(this.props.UserIsLoggedIn(true))}..
When I moved these dispatchers to under a lifecycle method, they worked fine without console.log wrapper... like this
componentWillMount() {
console.log(this.props.isUserLoggedIn)
this.props.UserIsLoggedIn(true)
}
I'm migrating to React Router 4.11 from 2.5.1. In the older version, my ISOMORPHIC/UNIVERSAL app retrieves the routes/components dynamically from a database at startup and I want to retain that same functionality with 4.11.
In the 2.5.1 version of react-router, I was able to load routes & components dynamically from the database by calling _createRouter with the routes parameter being the data retrieved from my db as such:
////////////////////////
// from client entry.js
////////////////////////
async function main() {
const result = await _createRouter(routes)
ReactDOM.render(
<Provider store={store} key="provider">
<Router history={history}>
{result}
</Router>
</Provider>
dest
);
}
//////////////////////
// from shared modules
//////////////////////
function _createRouter(routes) {
const childRoutes = [ // static routes, ie Home, ForgotPassword, etc];
////////////////////////
// crucial part here
////////////////////////
routes.map(route => {
const currComponent = require('../../client/components/' + route.route + '.js');
const obj = {
path: route.route,
component: currComponent
};
childRoutes.push(obj)
});
childRoutes.push({path: '*', component: NotFoundComponent});
return { path: '', component: .APP, childRoutes: childRoutes };
}
I am frustrated that I cannot find any docs on performing the same event in 4.11. Every example shows the routes hard coded like so:
render() {
return (
<Route path='/' component={Home} />
<Route path='/home' component={About} />
<Route path='/topics' component={Topics} />
)
}
This does not appear real-world to me especially for large apps.
Can anyone point me in the direction of accomplishing the same success I had in 2.5.1 with 4.11 insofar as being able to dynamically load my routes/components on the fly from a database?
Thanks much !
Maybe this code is the solution to this issue. "dynamic route import component"
import React, {
Component
} from "react";
const Containers = () => ( < div className = "containers" > Containers < /div>);
class ComponentFactory extends Component{constructor(props){super(props);this.state={RouteComponent:Containers}}
componentWillMount() {
const name = this.props.page;
import ('../pages/' + name + '/index').then(RouteComponent => {
return this.setState({
RouteComponent: RouteComponent.default
})
}).catch(err => console.log('Failed to load Component', err))
}
render() {
const {RouteComponent } = this.state;
return (<RouteComponent { ...this.props }/> );
}
}
export default ComponentFactory
<Route path="/:page"
render={rest => <ComponentFactory
page={rest.match.params.page}
{...rest}/>}/>
In the new version, each route is just a regular react component. Therefore, you should be able to dynamically create routes as you'd create any other component.
render() {
const { routes } = this.props;
return (
<div>
{
routes.map(route => <Route path={route.path} component={route.component} />)
}
</div>
);
}
You will also need to dynamically import your components before trying to render the routes.
I've been building a React app with a react-router 3.x setup like this:
<Route component={Global}>
<Route path="/" getComponent={(loc, cb) => loadRoute('Home', cb)} />
<Route path="/app" component={App}>
<IndexRoute
getComponent={(loc, cb) => loadRoute('AppHome', cb)}
/>
<Route
path="search"
getComponent={(loc, cb) => loadRoute('Search', cb)}
/>
</Route>
</Route>
I've had this working really well for chunking the containers within App. The loadRoute function wraps a System.import call adding containers/${name}/index.js.
However, adding a dynamic getComponent={loadRoute(...)} to App and navigating to /app causes Global to have no children; the JS chunks for App and AppHome are downloaded but nothing is mounted.
The loadRoute function:
const loadRoute = (container, callback) => {
return System
.import(`containers/${container}/index.js`)
.then(module => callback(null, module.default))
.catch(errorLoading);
};
For anybody seeing this question and being in the same position. I solved this a few months ago.
I used component instead of getComponent and passed it a component that is in charge of asynchronously loading the chunk, displaying a loading spinner and rendering it when available. The react-async-component package would also solve this issue.
Now, chunked routes can be nested many levels deep allowing for true code splitting in react-router 3.x.
components/AsyncComponent.js
import React from 'react';
const defaultLoadingElement = <div>Loading spinner...</div>;
const asyncComp = (getComponent, loadingElement = defaultLoadingElement) => {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if(!this.state.Component) {
getComponent().then(Component => {
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if(Component) {
return <Component {...this.props} />;
}
return loadingElement;
}
};
};
export default asyncComp;
routes.js
import React from 'react';
import { IndexRoute, Route } from 'react-router';
import asyncComp from 'components/AsyncComponent';
const loadRoute = container => asyncComp(() =>
import(`containers/${container}/index.js`)
.then(module => module.default)
);
const errorLoading = err => {
const error = new Error();
console.log(error.stack, err.stack);
throw new Error(`Dynamic page loading failed: ${err}`);
};
export default store => {
return (
<Route path="/admin" component={loadRoute('Admin')}>
<IndexRoute component={loadRoute('Admin/Dashboard')} />
<Route
path="accounts"
component={loadRoute('Admin/Accounts')}
/>
</Route>
);
};
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.