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>
Related
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.
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");
i am a react newbie, and every routing example i have found routes to components defined as const, but when using a component class with react router v4 the following error is thrown:
"TypeError: Cannot read property 'apply' of undefined
at new About (wuwemek.js:34:70)
..."
jsbin example
in the following example, routing to {Home} works fine, but routing to {About} throws the above error. relevant code below - please let me know if you need to see more:
var { BrowserRouter, Route, Link } = ReactRouterDOM;
const Home = () => <p>home</p>
class About extends React.Component {
render() {
return (<div>about</div>)
}
}
<Link to="/">home</Link>
<Link to="/about">about</Link>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
There is nothing wrong with your code.
Your jsbin's setting is incorrect. You should set it as JSX (React), not ES6/ Babel.
Working with the example in the README at
https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux
I have created this layout in my index:
// attach the redux dev tools extension for Chrome
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// apply router logic as middleware
const history = createHistory();
const router_mw = routerMiddleware(history);
// second arg to createStore is the inital redux store state
const store = createStore(
reducers,
initialState,
composeEnhancers(
applyMiddleware(
ReduxPromise,
ReduxThunk,
router_mw
)
));
// --> add routes for logs, opsec, etc. later
ReactDOM.render(
<Provider store={ store }>
<ConnectedRouter history={ history }> // <- error here
<div>
<Route exact path="/" component={ LoginScreen } />
<Route path="/comp1" component={ comp1 } />
<Route path="/comp2" component={ comp2 } />
</div>
</ConnectedRouter>
</Provider>
, document.getElementById('root')
);
Any attempt to compile it complains about { history } in the ConnectedRouter entry point:
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in. Check your code at index.js:52.
What am I missing? This is a react-router-redux retrofit - going back to react-router (and changing ConnectedRouter to BrowserRouter) and everything works as expected.
I used react-router-dom#4.2.2 and had the same problem until update react-router-redux to ^5.0.0-alpha.9.
If you are using react-router-dom#4 you should use react-router-redux#5, it is said in react-router-redux repo:
The next version of react-router-redux will be 5.0.0 and will be compatible with react-router 4.x. It is currently being actively developed over there. Feel free to help out!
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.