Im trying to get access to redux state but im also need props that passed from routing.
The example is: i need that props
const DefaultLayout = props => {
return (
<div>
</div>
)
}
because
<Route
path="/"
name="Home"
render={props => <DefaultLayout {...props} />}
/>
when i add redux state like: {auth: {user}} to access user data, its not working.
const DefaultLayout = (props , {auth: {user}}) => {
return (
<div>
</div>
)
}
...
DefaultLayout.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(DefaultLayout);
if i delete that props i will getting pathname error, any explanation and help? new to react.
I think you need to access the props this way :
const DefaultLayout = ({auth, location, history, ...otherProps}) => {
//example : console.log(otherProps.match);
return (
<div>
{auth.user}
</div>
)
}
Props passed by the Route component are merged with the props added by redux.
const DefaultLayout = (props , {auth: {user}}) => {
Components just get passed a single variable: props. If you want to access auth.user, it's found at props.auth.user. It's put there by mapStateToProps in cooperation with connect
const DefaultLayout = (props) => {
const { location, history, match, auth } = props;
return (
<div>
// something with the variables
</div>
)
}
const mapStateToProps = state => ({
auth: state.auth
})
export default connect(mapStateToProps)(DefaultLayout);
Related
I am working on testing a component using react-testing-library. The component has an alert which accepts a prop that comes from context to determine whether the alert is open or not.
const PersonRecord = () => {
const {
personSuccessAlert,
setPersonSuccessAlert,
updatePersonSuccessAlert,
setUpdatePersonSuccessAlert,
} = useContext(PeopleContext);
return (
{personSuccessAlert && (
<div className="person-alert-container">
<Alert
ariaLabel="person-create-success-alert"
icon="success"
open={personSuccessAlert}
/>
</div>
)}
)
}
So the above code uses context to pull the value of personSuccessAlert from PeopleContext. If personSuccessAlert is true the alert will display. My context file is set up as follows:
import React, { createContext, useState } from 'react';
export const PeopleContext = createContext();
const PeopleContextProvider = ({ children }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(false);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
export default PeopleContextProvider;
Now I am trying to develop a test which passes personSuccessAlert = true to the PersonRecord component.
Here is what I have been trying:
export function renderWithEmptyPerson(
ui,
{
providerProps,
path = '/',
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
},
) {
return {
...render(
<MockedProvider mocks={getEmptyPerson} addTypename={false}>
<PeopleContextProvider {...providerProps}>
<Router history={history}>
<Route path={path} component={ui} />
</Router>
</PeopleContextProvider>
</MockedProvider>,
),
};
}
describe('empty person record rendering', () => {
afterEach(cleanup);
test('empty person', async () => {
const providerProps = { value: true };
const { getByText, queryByText, queryByLabelText } = renderWithEmptyPerson(
PersonRecord,
{
providerProps,
route: 'people/6d6ed1f4-8294-44de-9855-2999bdf9e3a7',
path: 'people/:slug',
},
);
expect(getByText('Loading...')).toBeInTheDocument();
});
});
I have tried different variations of const providerProps = { value: true };. Replacing value with personSuccessAlert did not work.
Any advice or help is appreciated.
You are passing providerProps to the PeopleContextProvider, but the PeopleContextProvider is not doing anything with the props. You'll need to actually use those props, for example to set the initial state. You could try something like:
const PeopleContextProvider = ({ children, initialPersonSuccessAlert = false }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(initialPersonSuccessAlert);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
This would allow you to set the initial state of personSuccessAlert by passing in a initialPersonSuccessAlert prop. You could update your test like so:
const providerProps = { initialPersonSuccessAlert: true };
Alternatively, if you only wanted to make changes in your test file, you could consider updating the renderWithEmptyPerson function to use PeopleContext.Provider directly instead of the PeopleContextProvider component. That will allow you to set the context value however you like.
I show a cool feature of react where Component state can be used from where it's body where it's being used.
Here is an example from firebase Link
<FirebaseDatabaseNode
path="user_bookmarks/"
limitToFirst={this.state.limit}
orderByValue={"created_on"}
>
{d =>
return (
<React.Fragment>
<pre>Path {d.path}</pre>
</React.Fragment>
);
}}
</FirebaseDatabaseNode>
In this example FirebaseDatabaseNode is Component and we're accessing d inside it.
I want to implement similar so I could access data of component in similar way. Here is my attempt to implement similar state access for Dashboard Component.
export default function Dashboard({
children,
user
}: {
children: ReactNode;
user: any
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedMenu, setSelectedMenu] = React.useState(LinkItems[DEFAULT_SELECTED_ITEM])
//...
}
And I want to access selectedMenu inside of Dashboard in index.js
export default function () {
return (
<Dashboard user={user}>
{selectedMenu => {
return (
<div>{selectedMenu}</div>
)
}
}
</Dashboard>
)
}
But this is not working in my case and I don't know the exact terminology.
Finally while I explore the firebase source I found out that they are using render-and-add-props Library.
Dashboard.js
import { renderAndAddProps } from 'render-and-add-props';
//...
export default function Dashboard({
children,
user
}: {
children: ReactNode;
user: any
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedMenu, setSelectedMenu] = React.useState(LinkItems[DEFAULT_SELECTED_ITEM])
//...
return (
<div>
//...
// for rendering element with props
{renderAndAddProps(children, { 'selectedMenu': selectedMenu })}
</div>
)
}
//in index
export default function () {
return (
<Dashboard user={user}>
{({selectedMenu}) => { // {({ selectedMenu }: { selectedMenu: LinkItemProps }) if you're using typescript.
return (
<div>{selectedMenu}</div>
)
}
}
</Dashboard>
)
}
This is how my container looks like:
class Shipping extends React.PureComponent {
constructor(props) {
super(props)
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.shippingId !== this.props.match.params.shippingId) {
this.props.getShippingDetails(this.props.match.params.shippingId)
}
}
render = () => this.props.isLoading ? null : <ShippingView removeOrder={this.props.removeOrder} />
}
const mapStateToProps = ({ shippingDetails}) => ({
isLoading: shippingDetails.isLoading
})
const mapDispatchToProps = (dispatch) => ({
getShippingDetails: (id) => dispatch(shippingActions.getShippingDetails(id)),
removeOrder: (id) => dispatch(shippingActions.removeOrder(id))
})
export default () => Shared.accessPageWrapper([["acess:all"], ["admin:control", "navigation/shopping:view"]], (connect(mapStateToProps, mapDispatchToProps)(Shipping)), <Shared.AccessDeniedView />)
This is how my functional component looks like:
export const accessPageWrapper = (
permissionsAllowed = [],
component = null,
accessDeniedView,
accessDenied = true
) => {
const permissions = useSelector(state => state.user.permissions)
const permitted = permissionsAllowed.some((permission) => permission.every(el => permissions.includes(el)), false)
if (permitted) {
const Component = component
return <Component />
}
return accessDenied ? accessDeniedView : null
}
I'm not able to pass the props through the functional component as following:
const Component = component
return <Component {...props} />
Due to that issue, I'm getting the following error because my prop's prams are undefined.
Uncaught TypeError: Cannot read property 'params' of undefined
I have no idea how to fix this :/ Would you be so kind to help me?
Also, I don't want to change the above functional component to a class component.
Is there any way I can retrieve the props to the component? Thanks in advance!!
I think you are just missing the return of a component. Higher Order Components consume a component (and other possible parameters) and return a new, decorated component.
export const accessPageWrapper = (
permissionsAllowed = [],
component = null,
accessDeniedView,
accessDenied = true
) => (props) => { // <-- return a functional component
const permissions = useSelector((state) => state.user.permissions);
const permitted = permissionsAllowed.some(
(permission) => permission.every((el) => permissions.includes(el)),
false
);
if (component && permitted) { // <-- Ensure component exists!
const Component = component;
return <Component {...props} />; // <-- spread passed props to Component
}
return accessDenied ? accessDeniedView : null;
};
Probably need to update the export.
import { accessPageWrapper } from '.....';
...
export default accessPageWrapper(
[["acess:all"], ["admin:control", "navigation/shopping:view"]],
connect(mapStateToProps, mapDispatchToProps)(Shipping),
<Shared.AccessDeniedView />,
);
I have built an app on ReactJS 16.8.5 and React-Redux 3.7.2. When the app loads the app mounts, initial store is set and database subscriptions are set up against a Firebase Realtime Database. The app contains a sidebar, header and content section. By profiling the app using React Developer Tools I can see that the Sidebar is being rendered several times - triggering rerender of child components. I have implemented React.memo to avoid rerendring when props change.
From what I can see the props does not change, but the Sidebar still rerenders, which confuses me.
app.js
//Imports etc...
const jsx = (
<React.StrictMode>
<Provider store={store}>
<AppRouter />
</Provider>
</React.StrictMode>
)
let hasRendered = false
const renderApp = () => {
if (!hasRendered) { //make sure app only renders one time
ReactDOM.render(jsx, document.getElementById('app'))
hasRendered = true
}
}
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// Set initial store and db subscriptions
renderApp()
}
})
AppRouter.js
//Imports etc...
const AppRouter = ({}) => {
//...
return (
<React.Fragment>
//uses Router instead of BrowserRouter to use our own history and not the built in one
<Router history={history}>
<div className="myApp">
<Route path="">
<Sidebar />
</Route>
//More routes here...
</div>
</Router>
</React.Fragment>
)
}
//...
export default connect(mapStateToProps, mapDispatchToProps)(AppRouter)
Sidebar.js
//Imports etc...
export const Sidebar = (props) => {
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
if (id !== 'Sidebar') { return }
console.log('Profile', phase, actualDuration)
}
return (
<Profiler id="Sidebar" onRender={onRender}>
<React.Fragment>
{/* Contents of Sidebar */}
</React.Fragment>
</Profiler>
}
const mapStateToProps = (state) => {
console.log('Sidebar mapStateToProps')
return {
//...
}
}
const areEqual = (prevProps, nextProps) => {
const areStatesEqual = _.isEqual(prevProps, nextProps)
console.log('Profile Sidebar isEqual', areStatesEqual)
return areStatesEqual
}
export default React.memo(connect(mapStateToProps, mapDispatchToProps)(Sidebar),areEqual)
Console output
Sidebar mapStateToProps 2
Profile Sidebar mount 225
Sidebar mapStateToProps
Profile Sidebar isEqual true
Sidebar mapStateToProps
Profile Sidebar update 123
Sidebar mapStateToProps 2
Profile Sidebar update 21
Sidebar mapStateToProps
Profile Sidebar update 126
Sidebar mapStateToProps
Profile Sidebar update 166
Sidebar mapStateToProps
Profile Sidebar update 99
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Profile Sidebar update 110
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Profile Sidebar update 4
Why is the Sidebar rerendering eight times when the props has not changed? One rerender would be expected?
Kind regards /K
As commented; when mapStateToProps returns a new object it will re render the connected component even if no relevant values change.
This is because {} !== {}, an object with same props and values does not equal another object with same props and values because React compares object reference and not the values of the object. That is why you can't change state by mutating it. Mutating changes the values in the object but not the reference to the object.
Your mapStateToProps has to return a new reference at the 2nd level for it to re render with the same values, so {val:1} won't re render but {something:{val:1}} will.
The code below shows how not memoizing the result of mapStateToProps can cause re renders:
const { Provider, connect, useDispatch } = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const { useRef, useEffect, memo } = React;
const state = { val: 1 };
//returning a new state every action but no values
// have been changed
const reducer = () => ({ ...state });
const store = createStore(
reducer,
{ ...state },
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
const Component = (props) => {
const rendered = useRef(0);
rendered.current++;
return (
<div>
<div>rendered:{rendered.current} times</div>
props:<pre>{JSON.stringify(props)}</pre>
</div>
);
};
const selectVal = (state) => state.val;
const selectMapStateToProps = createSelector(
selectVal,
//will only re create this object when val changes
(val) => console.log('val changed') || { mem: { val } }
);
const memoizedMapStateToProps = selectMapStateToProps;
const mapStateToProps = ({ val }) =>
({ nonMem: { val } }); //re creates props.nonMem every time
const MemoizedConnected = connect(memoizedMapStateToProps)(
Component
);
//this mapStateToProps will create a props of {val:1}
// pure components (returned by connect) will compare each property
// of the prop object and not the props as a whole. Since props.val
// never changed between renders it won't re render
const OneLevelConnect = connect(({ val }) => ({ val }))(
Component
);
const Connected = connect(mapStateToProps)(Component);
const Pure = memo(function Pure() {
//props never change so this will only be rendered once
console.log('props never change so wont re render Pure');
return (
<div>
<Connected />
<MemoizedConnected />
<OneLevelConnect />
</div>
);
});
const App = () => {
const dispatch = useDispatch();
useEffect(
//dispatch an action every second, this will create a new
// state ref but state.val never changes
() => {
setInterval(() => dispatch({ type: 88 }), 1000);
},
[dispatch] //dispatch never changes but linting tools don't know that
);
return <Pure />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
The mapStateToProps function can also be optimised more by passing a function that returns a function. This way you can create a memoized selector when the component mounts. This can be used in list items (see code below).
const { useRef, useEffect } = React;
const {
Provider,
useDispatch,
useSelector,
connect,
} = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const state = {
data: [
{
id: 1,
firstName: 'Ben',
lastName: 'Token',
},
{
id: 2,
firstName: 'Susan',
lastName: 'Smith',
},
],
};
//returning a new state every action but no values
// have been changed
const reducer = () => ({ ...state });
const store = createStore(
reducer,
{ ...state },
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
//selectors
const selectData = (state) => state.data;
const selectPerson = createSelector(
selectData,
(_, id) => id, //pass second argument to select person by id
(people, _id) => people.find(({ id }) => id === _id)
);
//function that will create props for person component
// from person out of state
const asPersonProps = (person) => ({
person: {
fullName: person.firstName + ' ' + person.lastName,
},
});
//in ConnectedPerson all components share this selector
const selectPersonProps = createSelector(
(state, { id }) => selectPerson(state, id),
asPersonProps
);
//in OptimizedConnectedPerson each component has it's own
// selector
const createSelectPersonProps = () =>
createSelector(
(state, { id }) => selectPerson(state, id),
asPersonProps
);
const Person = (props) => {
const rendered = useRef(0);
rendered.current++;
return (
<li>
<div>rendered:{rendered.current} times</div>
props:<pre>{JSON.stringify(props)}</pre>
</li>
);
};
//optimized mapStateToProps
const mapPersonStateToProps = createSelectPersonProps;
const OptimizedConnectedPerson = connect(
mapPersonStateToProps
)(Person);
const ConnectedPerson = connect(selectPersonProps)(Person);
const App = () => {
const dispatch = useDispatch();
const people = useSelector(selectData);
const rendered = useRef(0);
rendered.current++;
useEffect(
//dispatch an action every second, this will create a new
// state ref but state.val never changes
() => {
setInterval(() => dispatch({ type: 88 }), 1000);
},
[dispatch] //dispatch never changes but linting tools don't know that
);
return (
<div>
<h2>app rendered {rendered.current} times</h2>
<h3>Connected person (will re render)</h3>
<ul>
{people.map(({ id }) => (
<ConnectedPerson key={id} id={id} />
))}
</ul>
<h3>
Optimized Connected person (will not re render)
</h3>
<ul>
{people.map(({ id }) => (
<OptimizedConnectedPerson key={id} id={id} />
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
Is it possible to call component in component (Like inception)
Example
Content.jsx
class Content extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
componentDidUpdate(prevProps) {
if(this.props.match.params.tab != prevProps.match.params.tab) {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
}
render() {
const {nav, match} = this.props;
let redirect = null;
return (
<div>
<ul>
{nav.some((item, key) => {
if (this.props.location.pathname != (match.url + item.path)) {
redirect = <Redirect to={(match.url + item.path)} />;
}
return true;
})}
{redirect}
{nav.map((item, key) => {
return <li key={key}>
<Link to={match.url + item.path}>{item.name}</Link>
</li>;
})}
<Switch>
{nav.map((item, key) => {
return <Route key={key} path={`${match.url}/list/:tab`} component={Content} />;
})}
</Switch>
</ul>
</div>
);
}
}
const mapStateToProps = (state, props) => {
const {fetchNav} = state;
const {
lastUpdated,
isFetching,
nav: nav
} = fetchNav[props.match.params.tab] || {
isFetching: true,
nav: []
};
return {
nav,
isFetching,
lastUpdated
}
};
export default connect(mapStateToProps)(withStyles(appStyle)(Content));
Actually when i do this, if my route match and call same "Content" component, it says : "this.props.dispatch is not a function"
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
Many thanks for you answers
Regards,
Thomas.
You are clearly not mapping dispatch to your props in your connect.
See the docs for redux' connect method:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
You can map the dispatch function to your props like like this:
const mapDispatchToProps = (dispatch) => ({ dispatch });
connect(mapStateToProps, mapDispatchToProps)...
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
You don't need a container. The separation of code between a container and a (view-) component is just a pattern and not required.
As a sidenote: I would recommend to use compose to combine your HOCs, see this or this.