How to access match inside mapStatetoProps - reactjs

How can I access match prop inside mapStateToProps?
import React from "react";
import "./collectionpage.styles.scss";
import { connect } from "react-redux";
import { selectCollection } from "../../redux/shop/shop.selector";
import { useMatch } from "react-router-dom";
import Collectionitem from
"../../components/collection-items/Collectionitem.component";
const CollectionPage = ({ collection }) => {
const match = useMatch();
const { title, items } = collection;
return (
<div className="collection-page">
<h2 className="title">{title}</h2>
<div className="items">
{items.map(item => <Collectionitem key={item.id} item={item}/>)}
</div>
</div>
)
}
mapStatetoProps = (state) => {
return ({
collection: selectCollection(match.params.colletionID)(state)
})
}
export default connect(mapStatetoProps)(CollectionPage);

mapStatetoProps allow you pass owner props to. So you can pass match to a component. in this case is CollectionPage
const ComponentA = () => {
const match = useMatch();
return (<CollectionPage match={match}/>)
}
then:
const mapStateToProps = (state, ownProps) => {
return {
collection: ownProps.match.params.colletionID
}
}

react-router-dom#5
Since it appears you are using react-router-dom#5 you'll have the withRouter Higher Order Component (HOC) available to you which can inject the route props. I've often combined this with the connect HOC to first wrap and inject the route props so they are accessible within the connect HOC.
Example:
import { connect } from "react-redux";
import { selectCollection } from "../../redux/shop/shop.selector";
import { withRouter } from "react-router-dom";
const CollectionPage = ({ collection }) => {
....
}
mapStateToProps = (state, props) => ({
collection: selectCollection(props.match.params.colletionID)(state),
});
export default withRouter(
connect(mapStateToProps)(CollectionPage)
);
Since you are also using Redux you may also find the compose function very handy here to compose all the HOCs over the component you want to decorate. It serves as a way to "unnest" all the HOCs your components may be using
Example:
import { compose } from 'redux';
import { connect } from "react-redux";
import { selectCollection } from "../../redux/shop/shop.selector";
import { withRouter } from "react-router-dom";
const CollectionPage = ({ collection }) => {
....
}
mapStateToProps = (state, props) => ({
collection: selectCollection(props.match.params.colletionID)(state),
});
export default compose(
withRouter,
connect(mapStateToProps),
)(CollectionPage);
react-router-dom#6
If using react-router-dom#6 you'll need to create your own custom withRouter replacement since it was removed in v6.
Example:
import { useParams, /* other hooks */ } from 'react-router-dom';
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
...
import { compose } from 'redux';
import { connect } from "react-redux";
import { selectCollection } from "../../redux/shop/shop.selector";
import { withRouter } from "../path/to/withRouter";
const CollectionPage = ({ collection }) => {
....
}
mapStateToProps = (state, props) => ({
collection: selectCollection(props.params.colletionID)(state),
});
export default compose(
withRouter,
connect(mapStateToProps),
)(CollectionPage);

Related

How can i pass useParams to mapStateToProps in React with react router v6?

In my Application I want to pass the parameter that I'm getting from useParams() to mapStateToProps function which I'm using with reselect Library .
I can hardcode the value instead of passing this parameter and everything works as expected. Also, I can pass useParams().routeName directly to mapState function and it's working. But in case of using ownProps it is not working. Here is my code :
const CollectionPage = ({collection}) => {
let params = useParams();
let collectionPath = params.routeName;
return (
<div className='collection-page'>
</div>
)
}
const mapStateToProps = (state, ownProps) => ({
collection: selectCollection(ownProps.collectionPath)(state)
});
export default connect(mapStateToProps)(CollectionPage);
With this code the return will be undefined but when I hardcode the value like code below its working :
const mapStateToProps = (state) => ({
collection: selectCollection(''Some Hard Coded Value'')(state)
});
Preferred
The preferred method would be to use the React hooks directly in the component. Instead of using the connect Higher Order Component use the useSelector hook to select/access the collection state. Refactor the selectCollection selector to return the state and do the filtering in the UI.
Example:
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const CollectionPage = () => {
const { routeName } = useParams();
const collection = useSelector(state => {
const collection = selectCollection(state);
// logic to return collection filtered by routeName, etc...
return <UI value here>
});
return (
<div className='collection-page'>
...
</div>
);
};
Alternative/Legacy
If you have the need to access path params in any mapStateToProps function, if you are using a lot of oder code for example, then you'll need to create another HOC to access the path params and have them injected as props so they are available in the mapStateToProps function.
Example:
import { useParams, /* other hooks */ } from "react-router-dom";
const withRouter = Component => props => {
const params = useParams();
// other hooks, useLocation, useNavigate, etc..
return <Component {...props} {...{ params, /* other injected props */ }} />;
};
export default withRouter;
...
import { compose } from 'redux';
import { connect } from 'react-redux';
import withRouter from '../path/to/withRouter';
const CollectionPage = ({ collection }) => {
return (
<div className='collection-page'>
...
</div>
);
};
const mapStateToProps = (state, { params: { routeName } = {} }) => ({
collection: selectCollection(routeName)(state),
});
export default compose(
withRouter, // <-- injects a params prop
connect(mapStateToProps) // <-- props.params accessible
)(BlogDetailsPage);

export 'withRouter' (imported as 'withRouter') was not found in 'react-router-dom'

I am totally a beginner in React and while practising I ran into this issue. Through searching, I found out that 'withRouter' is not supported anymore by 'react-router-dom v6'. But I can't figure out how to change my code compatibly to v6. Does anyone know how to change this code instead of using 'withRouter'? Thanks in advance!
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
const PostViewerContainer = ({ match }) => {
// 처음 마운트될 때 포스트 읽기 API요청
const { postId } = match.params;
const dispatch = useDispatch();
const { post, error, loading } = useSelector(({ post, loading }) => ({
post: post.post,
error: post.error,
loading: loading['post/READ_POST']
}));
useEffect(() => {
dispatch(readPost(postId));
// 언마운트될 때 리덕스에서 포스트 데이터 없애기
return () => {
dispatch(unloadPost());
};
}, [dispatch, postId]);
return <PostViewer post={post} loading={loading} error={error} />;
};
export default withRouter(PostViewerContainer);
enter image description here
That is correct, the withRouter Higher Order Component (HOC) was removed in react-router-dom#6.
Since PostViewerContainer is a function component, just use the React hooks directly. There's no need really for the withRouter HOC. In this case it's the useParams hook you need to import and use.
Example:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; // <-- import useParams hook
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
const PostViewerContainer = () => { // <-- remove match prop
// 처음 마운트될 때 포스트 읽기 API요청
const { postId } = useParams(); // <-- call hook and destructure param
const dispatch = useDispatch();
const { post, error, loading } = useSelector(({ post, loading }) => ({
post: post.post,
error: post.error,
loading: loading['post/READ_POST']
}));
useEffect(() => {
dispatch(readPost(postId));
// 언마운트될 때 리덕스에서 포스트 데이터 없애기
return () => {
dispatch(unloadPost());
};
}, [dispatch, postId]);
return <PostViewer post={post} loading={loading} error={error} />;
};
For reference, if you needed to still use an HOC for class based components you'd need to either convert them to function components or create a custom withRouter HOC.
Example:
import { useLocation, useNavigate, useParams } from 'react-router-dom';
const withRouter = Component => props => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
{...props}
location={location}
navigate={navigate}
params={params}
/>
);
};
export default withRouter;

How to set InitialState in React Testing Library

I am writing a test where I need to render a component, but the rendering of my component is not working and I am receiving this error:
Uncaught [TypeError: Cannot read property 'role' of undefined].
This is because in the componentDidMount function in my component I am checking if this.props.authentication.user.role === 'EXPERT'. However, this.props.authentication has user as undefined.
This is the correct initialState for my program, but for the test I want to set my initialState to have a user object. That is why I redefine initialState in my test. However, the component does not render with that new initialState.
Here is the testing file:
import { Component } from '../Component.js';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import { render, cleanup, waitFor } from '../../test-utils.js';
import '#testing-library/jest-dom/extend-expect';
afterEach(cleanup)
describe('Component Testing', () => {
test('Loading text appears', async () => {
const { getByTestId } = render(
<MemoryRouter><Component /></MemoryRouter>,
{
initialState: {
authentication: {
user: { role: "MEMBER", memberID:'1234' }
}
}
},
);
let label = getByTestId('loading-text')
expect(label).toBeTruthy()
})
});
Here is the Component file:
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
tasks: [],
loading: true,
}
this.loadTasks = this.loadTasks.bind(this)
}
componentDidMount() {
if (
this.props.authentication.user.role == 'EXPERT' ||
this.props.authentication.user.role == 'ADMIN'
) {
this.loadTasks(this.props.location.state.member)
} else {
this.loadTasks(this.props.authentication.user.memberID)
}
}
mapState(state) {
const { tasks } = state.tasks
return {
tasks: state.tasks,
authentication: state.authentication
}
}
}
I am also using a custom render function that is below
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import rootReducer from './_reducers'
import configureStore from './ConfigureStore.js';
import { createMemoryHistory } from 'history'
function render(ui, {
initialState = reducerInitialState,
store = configureStore({}),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
Perhaps I am coming super late to the party but maybe this can serve to someone. What I have done for a Typescript setup is the following (all this is within test-utils.tsx)
const AllProviders = ({
children,
initialState,
}: {
children: React.ReactNode
initialState?: RootState
}) => {
return (
<ThemeProvider>
<Provider store={generateStoreWithInitialState(initialState || {})}>
<FlagsProvider value={flags}>
<Router>
<Route
render={({ location }) => {
return (
<HeaderContextProvider>
{React.cloneElement(children as React.ReactElement, {
location,
})}
</HeaderContextProvider>
)
}}
/>
</Router>
</FlagsProvider>
</Provider>
</ThemeProvider>
)
}
interface CustomRenderProps extends RenderOptions {
initialState?: RootState
}
const customRender = (
ui: React.ReactElement,
customRenderProps: CustomRenderProps = {}
) => {
const { initialState, ...renderProps } = customRenderProps
return render(ui, {
wrapper: (props) => (
<AllProviders initialState={initialState}>{props.children}</AllProviders>
),
...renderProps,
})
}
export * from '#testing-library/react'
export { customRender as render }
Worth to mention that you can/should remove the providers that doesn't make any sense for your case (like probably the FlagsProvider or the HeaderContextProvider) but I leave to illustrate I decided to keep UI providers within the route and the others outside (but this is me making not much sense anyway)
In terms of the store file I did this:
//...omitting extra stuff
const storeConfig = {
// All your store setup some TS infer types may be a extra challenge to solve
}
export const store = configureStore(storeConfig)
export const generateStoreWithInitialState = (initialState: Partial<RootState>) =>
configureStore({ ...storeConfig, preloadedState: initialState })
//...omitting extra stuff
Cheers! 🍻
I am not sure what you are doing in the configure store, but I suppose the initial state of your component should be passed in the store.
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import { createMemoryHistory } from 'history'
function render(
ui,
{
initialState = reducerInitialState,
store = createStore(reducer,initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
I hope it will help you :)

Get url params in mapStateToProps when using recompose compose in reactjs

I have this URL: /user/Jd5Egbh...
I use react-router-dom for navigation. I have made a UserDetails component.
I use mapStateToProps to get url params like this:
function mapStateToProps({ users }, ownProps) {
return { user: users[ownProps.match.params.uid]};
}
It works fine but I would like to protect this route and allow access only for authenticated users.
In other components, I use a withAuthorization component and compose from recomposing to protect the route.
I tried to combine these two features like below:
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, { fetchUser })
)(UserDetails);
But ownProps is undefined in mapStateToProps function
How can I access URL params if I use compose to protect route?
EDIT1:
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
import { firebase, db, auth } from '../firebase';
const withAuthorization = (condition) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!condition(authUser)) {
this.props.history.push('/');
} else {
db.onceGetUser(authUser.uid).then(snapshot => {
let user_data = snapshot.val();
if (!user_data.admin) {
auth.doSignOut();
}
});
}
});
}
render() {
return this.props.authUser ? <Component /> : null;
}
}
const mapStateToProps = (state) => ({
authUser: state.auth.authUser,
});
return compose(
withRouter,
connect(mapStateToProps),
)(WithAuthorization);
}
export default withAuthorization;

mapStateToProps is undefined though connect imported syntax error

I have this error and can't really understand what could go wrong when {connect} imported and const mapStateToProps declared:
./src/Movies.js Syntax error: C:/projects/library/src/Movies.js:
Unexpected token (6:8)
6 | const mapStateToProps = (state) => ({
import React, { Component } from "react";
import { connect } from "react-redux";
import MovieItem from "./MovieItem";
class Movies extends Component {
const mapStateToProps = (state) => ({
movies: state.movies;
});
render() {
let movieItems = this.props.movies.map(movie => {
return <MovieItem movie={movie} />;
});
return <div className="Movies">{movieItems}</div>;
}
}
export default connect(mapStateToProps, null)(Movies);
You need to define mapStateToProps function outside of your React component
import React, { Component } from "react";
import { connect } from "react-redux";
import MovieItem from "./MovieItem";
class Movies extends Component {
render() {
let movieItems = this.props.movies.map(movie => {
return <MovieItem movie={movie} />;
});
return <div className="Movies">{movieItems}</div>;
}
}
const mapStateToProps = (state) => ({
movies: state.movies;
});
export default connect(mapStateToProps, null)(Movies);
A class member cannot be declared as a const, var or let. Also since you need to use it outside of the React component only, you should define it separately

Resources