v6 react-router returns `null` state in location when state given a function property - react-router

I am doing a nested routers layout. I have a parent component that needs to render a list of N children. Additionally, I want to pass a function (id: string) => Observable<X> from parent to child. Currently, I am doing:
// parent.tsx
function getStream(id: string): Observable<X>; // assume exists
function Parent() {
const child_links = children_data.map(child_data => (
<Link
to={...}
state={{ child_data, getStream }}
/>
));
...
// return some output with `child_links` embedded
}
// child.tsx
import { useLocation } from 'react-router';
function Child() {
const location = useLocation();
// The issue I am facing:
// returns { child_data: {...} } if I choose to omit `getStream` in `Parent`;
// returns `null` when `getStream` included; expected `getStream` to exist and be function I passed
console.log(location.state);
// some arbitrary rendering
}
//index.tsx, where Router defined:
ReactDOM.render(
<div>
<BrowserRouter>
<Routes>
<Route path='/' element={<Parent />}>
<Route path='children/:child_id' element={<Child />} />
</Route>
</Routes>
</BrowserRouter>
</div>,
document.getElementById('root')
);
Other ridiculous things I have tried:
Wrapping the function in an object, passed through constructor. Effect: causes same null state problem.
Wrapping the function in an object, as a method. Effect: does not cause null state, but the associated property of state ends up just being an empty object. this tells me that react-router is likely doing less-than-smart hash object cloning.
Things I think would be better avoided:
Just creating a static controller object that serves up the Observable
Using the Context system from react
Am I abusing state here? If Links are to be used in a nest router setting, am I expected not to share upstream state with downstream components?
Thanks!

As mentioned by #DrewReese, functions are not serializable, and thus cannot be sent through route state. Therefore, my solution to this issue is simply to introduce a getStream function in a location accessible to the Child.

Related

NextJS router solution for react-router Switch statement?

For those using NextJS, how do you achieve the same behavior for react-router Switch behavior.
ie...
react-router
BASE PAGE
--- header
-------some sub content
-------<SWITCH>
<Route .. for component 1 />
<Route .. for component 2 />
<Route .. for component 3 />
How is this achieved via NextJS?
If your aim is to persist a layout across the whole app you could use next js custom _app.js to achieve that. As Next js is file-based routing, creating pages inside pages/ will do the rest
As its documentation says
Next.js has a file-system based router built on the concept of pages.
When a file is added to the pages directory it's automatically available as a route.
The files inside the pages directory can be used to define most common patterns.
index-routes
I am also looking for a solution to use React's Switch component. But until I do, this is how I handle conditional switching:
export default function Page({ page, pageType }) {
// ...
//
switch (pageType) {
case 1:
return (
<Layout>
{beards.map((beard) => (
<BeardedDeveloper {...beard} key={beard.id} />
))}
</Layout>
);
case 2:
return (
<Layout>
<BeardedDeveloperDetail {...beard} key={beard.id} />
</Layout>
);
}
}
// for my use-case, `pageType` is defined in my `getStaticProps`
//
export async function getStaticProps({ params }) {
// ...
//
return {
props: {
page: page,
pageType: pageType
}
};
}
It's an int; but, it can easily be changed to enum, string, or any defined type.
You can achieve your desired result by following these simple steps:
You have to put your route components in the pages folder like
it's shown here
In Next.js, a page is a React Component exported from a file in the pages directory.
Pages are associated with a route based on their filename. For example, in development:
pages/index.js is associated with the / route.
pages/posts/first-post.js is associated with the /posts/first-post route.
You have to add a Layout component so you can add general components like header to every route and wrap your pages with the layout component like it's shown here in Nextjs tutorial.
src/components/HOC/layout.js
export default function Layout({ children }) {
return (
<React.Fragment>
<header>...</header>
{children}
</React.Fragment>
);
}
pages/profile.js
import Layout from "../src/components/HOC/layout.js";
const Profile = (props) => (
<Layout>
<div>User's profile</div>
</Layout>
);
export default Profile;
You can also add props like title to the layout component so you can change it for every specific route/page like below:
src/components/HOC/layout.js
export default function layout(Component, title) {
return (props) => (
<React.Fragment>
<header>{title}</header>
<Component {...props} />
</React.Fragment>
);
}
pages/profile.js
import layout from '../src/components/HOC/layout.js'
const Profile = (userName) => <div>Profile of {userName}</div>;
export default layout(Profile,"Your Profile Page");

React-router 5.2 failure to get parameter from URL

I have a component ViewRestaurant.js that is part of a React-Router (using 5.2). I'm trying to access a dynamic URL parameter (/view-restaurant/:id, the id)
Snippet from App.js (the main data loader):
return (
<Router>
<div>
<Route exact path="/" component={() => <Main
restaurants={this.state.restaurants}
account = {this.state.account} /> } />
<Route path="/view-restaurant/:id" component={() => <ViewRestaurant
restaurants = {this.state.restaurants}
account = {this.state.account} /> } />
</div>
</Router>
);
This is how I'm trying to put the Link, snippet from Main.js which displays a list of restaurants:
<Link
to={{
pathname: '/view-restaurant/' + key,
state: { id: key }
}}>
View
</Link>
But then, when I'm trying to access what I've passed, meaning the :id, which is the same as key in Main.js, it doesn't work.
Snippet from ViewRestaurant.js:
render() {
const restaurants = this.props.restaurants
console.log(restaurants[0].name) // this works.
console.log(this.state.id) // this throws the error.
// return removed for readability
}
The error: TypeError: this.state is null
According to the documentation, we should be able to create a state and pass it via link. I'm 100% sure I'm misinterpreting the documentation, but I just don't know how to solve this error.
I figured it out eventually.
Just don't use classic components with the render function that returns stuff.
The way in React Router v5 (v5.2 in my case) is to pass props via functions and get parameters via a special function.
For this specific issue I had, only had to use useParams() from the new React-Router documentation and completely refactor my code to use functions and hooks instead.
I highly recommend to ditch clasic classes that use the render() function and move to the improved way of coding using the new React-Router.
Youtube video that teaches you how to pass props
Documentation on useParams()
How I solved it:
function ViewRestaurant({ restaurants }) {
const { id } = useParams();
}
and in Main.js (note that the key is obtained via a simple js map)
<Link to={{ pathname: '/view-restaurant/' + key}}>View</Link>
and finally, App.js
<Route path="/view-restaurant/:id">
<ViewRestaurant
restaurants={this.state.restaurants} />
</Route>

How does one access state on a nested React component wrapped by an HOC?

I'm using Enzyme, and we can actually use the example component given in the docs as a foundation for my question.
Let's assume this <Foo /> component uses a <Link> component from ReactRouter and thus we need to wrap it in a <MemoryRouter> for testing.
Herein lies the problem.
it('puts the lotion in the basket', () => {
const wrapper = mount(
<MemoryRouter>
<Foo />
</MemoryRouter>
)
wrapper.state('name') // this returns null! We are accessing the MemoryRouter's state, which isn't what we want!
wrapper.find(Foo).state('name') // this breaks! state() can only be called on the root!
})
So, not exactly sure how to access local component state when using <MemoryRouter>.
Perhaps I'm performing an ignorant test? Is trying to get/set component state bad practice in testing? I can't imagine it is, as Enzyme has methods for getting/setting component state.
Just not sure how one is supposed to access the internals of a component wrapped in <MemoryRouter>.
Any help would be greatly appreciated!
So it seems with the latest release of Enzyme there is a potential fix for this issue of accessing state on a child component.
Let's say we have <Foo> (note the use of React Router's <Link>)
class Foo extends Component {
state = {
bar: 'here is the state!'
}
render () {
return (
<Link to='/'>Here is a link</Link>
)
}
}
Note: The following code is only available in Enzyme v3.
Revisiting the test code, we are now able to write the following
it('puts the lotion in the basket', () => {
const wrapper = mount(
<MemoryRouter>
<Foo />
</MemoryRouter>
)
expect(wrapper.find(Foo).instance().state).toEqual({
bar: 'here is the state!'
})
})
Using wrapper.find(Child).instance() we are able to access Child's state even though it is a nested component. In previous Enzyme versions we could only access instance on the root. You can also call the setState function on the Child wrapper as well!
We can use a similar pattern with our shallowly rendered tests
it('puts the lotion in the basket shallowly', () => {
const wrapper = shallow(
<MemoryRouter>
<Foo />
</MemoryRouter>
)
expect(wrapper.find(Foo).dive().instance().state).toEqual({
bar: 'here is the state!'
})
})
Note the use of dive in the shallow test, which can be run on a single, non-DOM node, and will return the node, shallow-rendered.
Refs:
https://github.com/airbnb/enzyme/issues/361
https://github.com/airbnb/enzyme/issues/1289
https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3
Thought it might be useful for you guys, as I stumbled upon this and have a fix.
In my case I have a component which is connected to redux.
class ComponentName extends Component {
...
}
export default connect(
mapStateToProps,
{
...
}
)(ComponentName );
connect() is obviously a HOC component.
So how do we access the "ComponentName" here?
Very simple:
component
.find(ComponentName)
.children()
.first()
.props() // returns ComponentName's props

passing props from <Router> to children

My LanguageProvider component adds messages property to its children. I was able to pass this parameter down to route components with the following code:
function createElement(Component, props) {
// TODO accessing this.propName seems to be an undocumented feature
return <Component {...props} messages={this.messages} />;
}
render(
<LanguageProvider>
<Router history={history} createElement={createElement}>
<Route path="/" component={App}/>
</Router>
</LanguageProvider>,
document.getElementById('root')
);
I haven't found accesing this.propertyName to be mentioned in the documentation and I've found this feature only by the trial-and-error approach. Also I don't know how to pass all the extra props from Router to route components like {...this.props}.
For completeness this is render method of LanguageProvider:
render() {
return React.cloneElement(this.props.children, {...this.props, messages: {foo: 'bar'} });
}
Any ideas how to do it standard way?

With React Redux Router, how should I access the state of the route?

With react-router-redux, it appears as though the only way to get routing information is through props only. Is this right?
Here's roughly what I am doing in my app right now:
<Provider store={Store}>
<Router history={history}>
<Route path="/" component={App}>
<Route path="child/:id" />
</Route>
</Router>
</Provider>
App
const App = (props) =>
<div className="app">
<Header />
<Main {...props}/>
<Footer />
</div>
Main
const Main = (props) =>
<div>
<MessageList {...props}/>
</div>
MessageList
let MessageList = (props) => {
const {id} = props;
// now I can use the id from the route
}
const mapStateToProps = (state, props) => {
return {
id: props.params.id
};
};
MessageList = connect(mapStateToProps)(MessageList)
What I would like to do, is remove {...props} from all of my components, and turn MessageList into this:
let MessageList = (props) => {
const {id} = props;
// now I can use the id from the route
}
const mapStateToProps = (state) => {
return {
id: state.router.params.id
};
};
MessageList = connect(mapStateToProps)(MessageList)
Having to pass down props in everything feels like a big step back for how clean Redux made my application. So if passing params is correct, I'm wondering why thats preferable?
My specific case that brought this up:
I have an UserInput component that sends a message (dispatches a SEND_MESSAGE action). Depending on the current page (chat room, message feed, single message, etc) the reducer should put it in the correct spot. But, with react-redux-router, the reducer doesn't know about the route, so it can't know where to send the message.
In order to fix this I need to pass the props down, attach the id to my SEND_MESSAGE action, and now the otherwise simple UserInput is handling business logic for my application.
Rather than address your question (how to read the state), I will address your problem itself (how to dispatch different actions depending on the current route).
Make your UserInput a presentational component. Instead of dispatching inside it, let is accept onSend prop that is a callback provided by the owner component. The input would call this.props.onSend(text) without knowing anything about Redux or routes.
Then, make MessageList also a presentational component that accepts onSendMessage as a prop, and forwards it to UserInput. Again, MessageList would be unaware of routes, and would just pass it down to <UserInput onSend={this.props.onSendMessage} />.
Finally, create a couple of container components that wrap MessageList for different use cases:
ChatRoomMessageList
const mapDispatchToProps = (dispatch) => ({
onSendMessage(text) {
dispatch({ type: 'SEND_MESSAGE', where: 'CHAT_ROOM', text })
}
})
const ChatRoomMessageList = connect(
mapStateToProps,
mapDispatchToProps
)(MessageList)
FeedMessageList
const mapDispatchToProps = (dispatch) => ({
onSendMessage(text) {
dispatch({ type: 'SEND_MESSAGE', where: 'FEED', text })
}
})
const FeedMessageList = connect(
mapStateToProps,
mapDispatchToProps
)(MessageList)
Now you can use these container components in your route handlers directly. They will specify which action is being dispatched without leaking those details to the presentational components below. Let your route handlers take care of reading IDs and other route data, but try to avoid leaking those implementation details to the components below. It’s easier in most cases when they are driven by props.
Addressing the original question, no, you should not attempt to read the router parameters from Redux state if you use react-router-redux. From the README:
You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.
There are some experimental projects that do keep the whole routing state in Redux but they have other drawbacks (for example, React Router state is unserializable which is contrary to how Redux works). So I think the suggestion I wrote above should solve your use case just fine.