Testing connected react component that needs params - javascript

I'm trying to test a connected react component that needs a props.params.id to call action creators. When I try to test that the component is connected to the store I get "Uncaught TypeError: Cannot read property 'id' of undefined"
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={PostsIndex}/>
<Route path="posts/:id" component={ShowPost}/>
</Route>
</Router>
</Provider>
, document.querySelector('.container'));
describe('ConnectedShowPost', ()=> {
let initialState = {
posts: { postsList: postsListData, post: postData, error: '' },
comments: {showComments: false},
auth: {authenticated: true}
};
store = mockStore(initialState);
connectedShowPost = TestUtils.renderIntoDocument(<Provider store={store}><ConnectedShowPost/></Provider>);
showPost = TestUtils.scryRenderedComponentsWithType(connectedShowPost, ShowPost)[0];
expect(showPost.props.posts.post).toEqual(postData);
})
I've tried including params in the store but that doesn't work since params is not hooked up to the store when used inside the component.
I've also tried passing it in as an ownProps argument and that didn't work either.
Passing it in to the ConnectedShowPost component as a props causes all other state items in the store to be undefined..
Also tried to directly set showPost.props.params = {id: '123} which didnt work either..
Any ideas on how to get this test to work?

If your connected component receives a param like this:
function mapStateToProps(state, ownProps)
{
const { match: { params } } = ownProps;
console.log(params.id);
return {
......
};
}
Then, In your test it's possible to inject that param as traditional props. Check out the next example:
wrapper = mount(
<MemoryRouter>
<ConnectedContainer
store={store}
match={
{
params: {
id: 1
}
}
}
/>
</MemoryRouter>
);

Related

React Routes and Components prop mix

I have the following line of code:
<Route path="/users/:id" exact ><User url={structure.users} /></Route>
In User component I do the following:
function User(props, {match}) {
useEffect(() => {
console.log(match);
console.log(props);
}, [props]);
// ...
}
I removed a bit of the code because I only want to show what is necessary but here I see that match is undefined but it should have match.params.id from the link. How can I achieve this so i can make the full url with the url with the attached id?
React components receive only a single props object argument.
Use the Route's render prop so you can pass the route props on to the component.
<Route
path="/users/:id"
exact
render={routeProps => <User url={structure.users} {...routeProps} />}
/>
Alternatively you can use the useParams React hook to access match.params of the current Route.
import { ...., useParams, .... } from 'react-router-dom';
function User(props) {
const { id } = useParams();
useEffect(() => {
console.log(id);
console.log(props);
}, [props]);
// ...
}

React: Unable to navigate views with router after changing the state of Context (useContext)

I have a simple setup to test the use of the useContext hook, when you want to change the context value in child components.
A simple Context is defined in its own file like such:
import React from 'react'
const DataContext = React.createContext({})
export const DataProvider = DataContext.Provider
export default DataContext
Then I wrap my router in a provider in a component that exposes its state to use as a reference for the ContextProvider, as such:
import { DataProvider } from './dataContext.js'
export default function App(props) {
const [data, setData] = useState("Hello!")
const value = { data, setData }
const hist = createBrowserHistory();
return (
<DataProvider value={value}>
<Router history={hist}>
<Switch>
<Route path="/admin" component={Admin} />
<Redirect from="/" to="/admin/services" />
</Switch>
</Router>
</DataProvider>
)
}
Finally I have two Views that I am able to navigate between initially, one of them showcasing the context value, as well as containing a button to change it:
export default function EndpointView(props) {
const { data, setData } = useContext(DataContext)
return (
<div>
<h1>{data}!</h1>
<Button onClick={() => setData(Math.random())}>Update context state</Button>
</div>
)
}
The functionality seems to work, as the showcases text is updated.
The problem is, when I have clicked the button, I can no longer navigate in my navbar, even though the url is changing. Any ideas as to why?
This is showcased in this picture, where the url is corresponding to the top-most item in the side bar, even though we are stuck in the "endpoint view"-component.
Edit:
So the routing works by including a switch in the Admin layout:
const switchRoutes = (
<Switch>
{routes.map((prop, key) => {
if (prop.layout === "/admin") {
return (
<Route
path={prop.layout + prop.path}
component={prop.component}
key={key}
/>
);
}
return null;
})}
<Redirect from="/admin" to="/admin/services" />
</Switch>
);
Where the routes (which we .map) are fetched from another file that looks like this:
const dashboardRoutes = [
{
path: "/services",
name: "Services view",
icon: AccountBalance,
component: ServicesView,
layout: "/admin"
},
{
path: "/endpoint",
name: "Endpoint view",
icon: FlashOn,
component: EndpointView,
layout: "/admin"
}
];
export default dashboardRoutes;
I was able to solve this issue.
I suspect the problem was that updating the state reloaded the root router component which caused some issues.
Instead I moved the DataProvider tag one step down the tree, to wrap the switch in the Admin component.

How to get which route called a component

I am using react-route-dom, I want to implement this config:
<Route exact path='/test/:id' component={MyComponent}/>
<Route exact path='/test2/:id' component={MyComponent}/>
then in MyComponent, I want to make something different depending on which route is calling it.
const MyComponent = ({info: {param}, info2: {param1, param2}}) => {
return (
<div>
</div>
)
}
MyComponent.propTypes = {
info: PropTypes.object.isRequired,
info2: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
info: state.info,
info2: state.info2
});
export default connect(mapStateToProps, {})(MyComponent);
How can I get the test or test2 params?
You can get access to the entire path using props.location
You can send it via props:
<Route exact path='/test/:id'>
<MyComponent route={'test'} />
</Route>

Why is my React component mounting after each action?

I have a React component called Home which is calling an action to fetch some groups when the component mounts.
I am calling an action as follows:
componentDidMount() {
const { fetchRecentGroups } = this.props;
fetchRecentGroups();
}
My reducer is picking up each action perfectly fine and is returning a state as follows:
switch(action.type) {
case REQUEST_GROUPS:
return {
...state,
loadState: FETCHING
};
case REQUEST_GROUPS_SUCCESS:
return {
...state,
loadState: SUCCESS,
groups: action.data.groups,
totalResults: action.data.totalResults
};
default:
return state;
}
I am also using the connect HOC on this component as follows:
const mapStateToProps = (state) => {
return {}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchRecentGroups: () => {
dispatch(actions.fetchRecentGroups())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
The Home component is placed inside a Route like this:
<Route
exact={true}
path="/"
component={Home}
/>
My problem is that each time the reducer returns a state, the componentDidMount is called again and again in a loop. I would expect the mount to happen only once at the first load.
If I put componentDidUpdate and componentWillReceiveProps functions in my component, they are never called (only componentDidMount) so I am not able to compare props.
Does anyone know why this may be happening?
EDIT:
I have found my problem to be caused by this piece of code in my route:
const RouteBlock = () => {
if(errorSettings) {
return <Error {...errorSettings} />
}
return (
<div className={styles.RouteBlock}>
<Route exact={true} path="/" component={Home} />
<Route path="/search" render={() => <div>SEARCH</div>} />
</div>
);
};
return <Router><RouteBlock /></Router>
I changed it to:
return <Router>{RouteBlock()}</Router>
Every time you render your component, you are immediately calling function that is setting new state and you are triggering re-rendering of your component. Maybe you should use shouldComponentUpdate life cycle method that will check is your old state the same as new one.
Check out official docs: https://reactjs.org/docs/react-component.html#shouldcomponentupdate

React Router v4: Sending requests when navigation changes

I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)

Categories