I have looked a lot of questions and answers on the site concerning an issue that I am experiencing.
I have the usual setup with React, React Router and Redux. My top level component is as follows.
// Imports
const reducers = {
main: baseReducer,
form: formReducer,
};
const reducer = combineReducers(reducers);
const store = createStore(
reducer,
applyMiddleware(thunk),
);
store.dispatch(actions.auth.setCurrentUser());
// store.dispatch(actions.api.fetchLineup());
ReactDOM.render(
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>
,
document.getElementById('root')
);
Inside my App.jsx I have the following code:
import React from 'react';
import Main from './Main';
const App = () => (
<div>
<Main />
</div>
);
export default App;
My Main.jsx
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import GroupingContainer from '../containers/Grouping';
import HomeContainer from '../containers/Home';
const Main = () => (
<main>
<Switch>
<Route exact path='/' component={HomeContainer} />
<Route path='/groupings/:id' component={GroupingContainer} />
</Switch>
</main>
);
export default Main;
And finally I have my Grouping.jsx and GroupingContainer.jsx
import React, { Component } from 'react';
function loadGrouping(props, groupingId) {
const grouping = props.main.groupings.find(
(g) => g.id === groupingId
);
if (!grouping) {
props.dispatch(api.fetchGrouping(groupingId));
}
}
class Grouping extends Component {
constructor(props) {
super(props);
loadGrouping(props, props.groupingId);
}
componentWillReceiveProps(nextProps) {
console.log('Next: ', nextProps);
if (nextProps.match && nextProps.match.params.id !== this.props.match.params.id) {
loadGrouping(this.props, nextProps.groupingId);
}
}
render() {
const grouping = this.props.main.groupings.find(
(lg) => lg.id === this.props.groupingId
);
return (
<div>
<h1>{grouping.name}</h1>
</div>
);
}
}
export default Grouping;
GroupingContainer.jsx
import { connect } from 'react-redux';
import Grouping from '../components/Grouping';
const mapStateToProps = (state, ownProps) => {
return {
groupingId: parseInt(ownProps.match.params.id, 10),
...state,
};
};
const mapDispatchToProps = (dispatch) => ({
dispatch,
});
const GroupingContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(Grouping);
export default GroupingContainer;
After the request it fires another action that adds the returned grouping to the store and into an array of groups state.main.groups
I am having 2 problems. When I browse from the root path to one of the groupings, the following flow:
http://localhost:3000 -> http://localhost:3000/#/groupings/19
I receive the message: Uncaught TypeError: Cannot read property 'name' of undefined for a brief second until the API request finishes and populates the {grouping.name} and when I do a complete refresh of the page on a grouping URL http://localhost:3000/#/groupings/19 the application does not load at all and gives them same Uncaught TypeError: Cannot read property 'name' of undefined
I have been using React for around 2 months and have really started using API requests on Component loads. I can not really figure out where to place the API request properly to prevent the view rendering before it has finished and erroring out.
Any help would be appreciated!
Thanks!
Try to change render of Grouping Component like this.
render() {
const grouping = this.props.main.groupings.find(
(lg) => lg.id === this.props.groupingId
);
return (
<div>
<h1>{grouping ? grouping.name : ""}</h1>
</div>
);
}
Related
I'm using react-router-dom version 6.0.2 here and the "Render" props isn't working, every time I got to the url mentioned in the Path of my Route tag it keeps throwing me this error - "Matched leaf route at location "/addRecipe" does not have an element. This means it will render an with a null value by default resulting in an "empty" page.". Can someone please help me with this issue
import './App.css';
import Home from './components/Home';
import AddRecipe from './components/AddRecipe';
import items from './data';
import React, { useState } from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
const App = () => {
const [itemsList, setItemsList] = useState(items)
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
}
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a!== itemToRemove))
}
return (
<Router>
<Routes>
<Route path="/addRecipe" render={ ({history}) => {
return (<AddRecipe onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
} } />);
} } />
</Routes>
</Router>
);
}
export default App;
In react-router-dom version 6, you should use element prop for this.
I suggest your read their document on upgrading from version 5 where they explain the changes.
For your problem, you should write something like this:
<Route
path="/addRecipe"
element={
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
}
/>
}
/>
The Route component API changed significantly from version 5 to version 6, instead of component and render props there is a singular element prop that is passed a JSX literal instead of a reference to a React component (via component) or a function (via render).
There is also no longer route props (history, location, and match) and they are accessible only via the React hooks. On top of this RRDv6 also no longer surfaces the history object directly, instead abstracting it behind a navigate function, accessible via the useNavigate hook. If the AddRecipe component is a function component it should just access navigate directly from the hook. If it unable to do so then the solution is to create a wrapper component that can, and then render the AddRecipe component with the corrected onAddRecipe callback.
Example:
const AddRecipeWrapper = ({ addRecipe }) => {
const navigate = useNavigate();
return (
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
navigate('/');
}}
/>
);
};
...
const App = () => {
const [itemsList, setItemsList] = useState(items);
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
};
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a !== itemToRemove))
};
return (
<Router>
<Routes>
<Route
path="/addRecipe"
element={<AddRecipeWrapper addRecipe={addRecipe} />}
/>
</Routes>
</Router>
);
};
I'm rendering components from my external (node_modules) pattern library. In my main App, I'm passing my Link instance from react-router-dom into my external libraries' component like so:
import { Link } from 'react-router-dom';
import { Heading } from 'my-external-library';
const articleWithLinkProps = {
url: `/article/${article.slug}`,
routerLink: Link,
};
<Heading withLinkProps={articleWithLinkProps} />
In my library, it's rendering the Link as so:
const RouterLink = withLinkProps.routerLink;
<RouterLink
to={withLinkProps.url}
>
{props.children}
</RouterLink>
The RouterLink seems to render correctly, and even navigates to the URL when clicked.
My issue is that the RouterLink seems to have detached from my App's react-router-dom instance. When I click Heading, it "hard" navigates, posting-back the page rather than routing there seamlessly as Link normally would.
I'm not sure what to try at this point to allow it to navigate seamlessly. Any help or advice would be appreciated, thank you in advance.
Edit: Showing how my Router is set up.
import React from 'react';
import { hydrate, unmountComponentAtNode } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'react-router-redux';
import RedBox from 'redbox-react';
import { Route } from 'react-router-dom';
import { Frontload } from 'react-frontload';
import App from './containers/App';
import configureStore from './redux/store';
import withTracker from './withTracker';
// Get initial state from server-side rendering
const initialState = window.__INITIAL_STATE__;
const history = createBrowserHistory();
const store = configureStore(history, initialState);
const mountNode = document.getElementById('react-view');
const noServerRender = window.__noServerRender__;
if (process.env.NODE_ENV !== 'production') {
console.log(`[react-frontload] server rendering configured ${noServerRender ? 'off' : 'on'}`);
}
const renderApp = () =>
hydrate(
<AppContainer errorReporter={({ error }) => <RedBox error={error} />}>
<Provider store={store}>
<Frontload noServerRender={window.__noServerRender__}>
<ConnectedRouter onUpdate={() => window.scrollTo(0, 0)} history={history}>
<Route
component={withTracker(() => (
<App noServerRender={noServerRender} />
))}
/>
</ConnectedRouter>
</Frontload>
</Provider>
</AppContainer>,
mountNode,
);
// Enable hot reload by react-hot-loader
if (module.hot) {
const reRenderApp = () => {
try {
renderApp();
} catch (error) {
hydrate(<RedBox error={error} />, mountNode);
}
};
module.hot.accept('./containers/App', () => {
setImmediate(() => {
// Preventing the hot reloading error from react-router
unmountComponentAtNode(mountNode);
reRenderApp();
});
});
}
renderApp();
I've reconstructed your use case in codesandbox.io and the "transition" works fine. So maybe checking out my implementation might help you. However, I replaced the library import by a file import, so I don't know if that's the decisive factor of why it doesn't work without a whole page reload.
By the way, what do you mean exactly by "seamlessly"? Are there elements that stay on every page and should not be reloaded again when clicking on the link? This is like I implemented it in the sandbox where a static picture stays at the top on every page.
Check out the sandbox.
This is the example.js file
// This sandbox is realted to this post https://stackoverflow.com/q/59630138/965548
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Heading } from "./my-external-library.js";
export default function App() {
return (
<div>
<img
alt="flower from shutterstock"
src="https://image.shutterstock.com/image-photo/pink-flowers-blossom-on-blue-600w-1439541782.jpg"
/>
<Router>
<Route exact={true} path="/" render={Welcome} />
<Route path="/article/coolArticle" component={CoolArticleComponent} />
</Router>
</div>
);
}
const Welcome = () => {
const articleWithLinkProps = {
url: `/article/coolArticle`,
routerLink: Link
};
return (
<div>
<h1>This is a super fancy homepage ;)</h1>
<Heading withLinkProps={articleWithLinkProps} />
</div>
);
};
const CoolArticleComponent = () => (
<div>
<p>This is a handcrafted article component.</p>
<Link to="/">Back</Link>
</div>
);
And this is the my-external-library.js file:
import React from "react";
export const Heading = ({ withLinkProps }) => {
const RouterLink = withLinkProps.routerLink;
return <RouterLink to={withLinkProps.url}>Superlink</RouterLink>;
};
I'm having an issue where when I want to dispatch an action, fetchRewardByPromoCodeAction it's saying that the action I want to dispatch is not a function.
In the the form, I use the the event handleer onSubmit then use handleSubmit. I noticed that my props becomes undefined so, which leads me to thinking that the connect function isn't working as expected. Any assistance would be helpful. Here's the code.
import React, { Component } from 'react';
import { connect
} from 'react-redux';
import PromoCodeInput from 'components/promoCodeForm/PromoCodeInput';
import { fetchRewardByPromoCode, resetValidations } from 'rewards/ducks';
import Button from 'button/Button';
export class AdminRewardPage extends Component<Props> {
constructor(props) {
super(props);
this.state = {
promoCodeText: '',
};
}
onPromoCodeChange = (event) => {
this.setState({
promoCodeText: event.target.value,
});
const { resetValidationsAction } = this.props;
resetValidationsAction();
};
handleSubmit = (event) => {
event.preventDefault()
const { fetchRewardByPromoCodeAction } = this.props;
const { promoCodeText } = this.state;
fetchRewardByPromoCodeAction(promoCodeText);
}
render() {
const { promoCodeText } = this.state
return (
<div>
<h1>AdminRewardPage</h1>
<form onSubmit={this.handleSubmit}>
<PromoCodeInput inputValue={promoCodeText} onChangeHandler={this.onPromoCodeChange} />
<Button type="submit" label="Find By PromoCode" fullWidth />
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
resetValidationsAction: () => dispatch(resetValidations()),
fetchRewardByPromoCodeAction: (promoCodeText) => dispatch(fetchRewardByPromoCode(promoCodeText)),
});
export default connect(null, mapDispatchToProps)(AdminRewardPage);
in rewards/ducks.js
export const fetchRewardByPromoCode = (promoCode: string): FSAModel => ({
type: FETCH_REWARD_BY_PROMOCODE,
payload: promoCode,
})
---EDIT--WITH--ANSWER---
#Bartek Fryzowicz below helped lead me to right direction. I forgot to look in my index.js file where my routes are
Previously I had
import { AdminRewardPage } from 'scenes/AdminRewardPage'
instead of
import AdminRewardPage from 'scenes/AdminRewardPage'
<Router history={ history }>
<Switch>
<Route exact path={ `/rewards` } component={AdminRewardPage} />
</Switch>
</Router>
I didn't bother to look how I was importing it.
LESSON
Look at where and HOW your files are being imported and exported.
You're trying to call fetchRewardByPromoCode function inside mapDispatchToProps but such function (fetchRewardByPromoCode) is not declared inside mapDispatchToProps scope nor in parent scope. Maybe you have forgotten to import it?
Answer update:
Please make sure that when you use the component you use default export (not named export) since named export is the presentational component not connected to redux store. You have to use container component connected to redux so make sure you import it like this:
import AdminRewardPage from '/somePath'
not
import { AdminRewardPage } from '/somePath'
I'm having issues accessing a parameter called bookId from the Reader.js component. The parameter is passed down from BookCarouselItem.js using react-router. Reader.js is a connected component.
I'm not sure if that makes a difference, but does react-router work with redux connected components? Or do I need to use something like connected-react-router?
I've tried to refer to similar questions but wasn't able to find a solution, help would be greatly appreciated.
App.js
import React, { Component } from 'react'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import { routes } from 'constants/index';
import Reader from 'components/reader/Reader'
Class App extends Component {
render() {
return (
<div className='container-fluid main-container'>
<Router>
<div>
<Route
path={'/reader/:bookId'}
component={() => <Reader />}
/>
</div>
</Router>
</div>
);
}
}
export default App;
BookCarouselItem.js
import React from 'react'
import { Link } from 'react-router-dom'
export class BookCarouselItem extends React.Component {
render() {
const { bookThumbnail } = this.props;
const { name, numberOfSections } = bookThumbnail;
const bookId = 0;
return (
<Link className='book-carousel-link' to={`/reader/${bookId}`}>
<div className='book-info-overlay'>
<h5>{name}</h5>
<span>{numberOfSections} Sections</span>
</div>
</Link>
);
}
}
export default BookCarouselItem;
Reader.js
import React from 'react'
import { connect } from 'react-redux'
import { compose } from 'recompose'
export class Reader extends React.Component {
render() {
const { match, pageLevel } = this.props;
console.log(match); // undefined
return (
<div>
<div className='reader-body'>
<Book bookId={match.params.bookId}
pageLevel={pageLevel}
bank={bank}/>
</div>
);
}
}
Const mapStateToProps = (state) => {
return {
metadata: state.book.metadata,
pageLevel: state.book.pageLevel
}
};
const authCondition = (authUser) => !!authUser;
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, mapDispatchToProps),
)(Reader);
You can just give the component to the component prop and the route props will be passed down to the component automatically.
<Route
path="/reader/:bookId"
component={Reader}
/>
If you want to render something that is not just a component, you have to pass down the route props manually.
<Route
path="/reader/:bookId"
render={props => <Reader {...props} />}
/>
I'm not sure but maybe mapStateToProps rewrite you props so could you please first read this issue
There's something driving me crazy in React, and I need your help. Basically, when the user clicks "My Profile", I want to redirect to the user's profile. To do that, I use the following snippet
viewProfile = () => {
console.log('My USER ID: ', this.props.userId);
this.props.history.push(`/profile/${this.props.userId}`);
}
This should work. However, although the URL is changing to the correct URL when 'My Profile' is clicked, the page isn't appearing. It just stays as the old page.
After googling for a while, I know it's something to do with redux ... However, I can't find a solution. (this.props.userId is coming from a Layout component, which is in turn getting it from redux store)
Here's my code:
// React
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
// Redux
import { connect } from 'react-redux';
import * as actions from '../../../store/actions/';
// Containers and Components
...// more imports
const styles = // styles
class NavBar extends Component {
handleAuthentication = () => {
if (this.props.isAuthenticated) {
this.props.history.push('/logout');
} else {
this.props.history.push('/login');
}
};
viewProfile = () => {
console.log('My USER ID: ', this.props.userId);
this.props.history.push(`/profile/${this.props.userId}`);
};
render() {
let buttons = (
<Auxillary>
{this.props.isAuthenticated ? (
<RaisedButton
label="MY PROFILE"
primary={true}
style={styles.button}
onClick={this.viewProfile} // THIS IS THE METHOD
/>
) : null}
<RaisedButton
backgroundColor={grey900}
label={this.props.isAuthenticated ? 'LOGOUT' : 'LOGIN'}
labelColor={grey50}
style={styles.button}
onClick={this.handleAuthentication}
/>
</Auxillary>
);
let itemSelectField = null;
if (this.props.location.pathname === '/items') {
itemSelectField = (
<ItemSelectField
onSelectTags={tags => this.props.filterItemsByTagName(tags)}
/>
);
}
let bar = null;
if (
this.props.location.pathname !== '/login' &&
this.props.location.pathname !== '/register'
) {
bar = (
<AppBar
style={styles.appBar}
title={itemSelectField}
iconElementLeft={
<img style={styles.logoHeight} src={Logo} alt="logo" />
}
iconElementRight={buttons}
/>
);
}
return <Auxillary>{bar}</Auxillary>;
}
}
const mapDispatchToProps = dispatch => {
return {
filterItemsByTagName: selectedTags =>
dispatch(actions.filterItemsByTagName(selectedTags))
};
};
export default withRouter(connect(null, mapDispatchToProps)(NavBar));
You can find the entire app here: https://github.com/Aseelaldallal/boomtown/tree/boomtown-backend-comm
I'm going crazy trying to fix this. help.
I think has to do with you using <BrowserRouter>. It creates its own history instance, and listens for changes on that. So a different instance will change the url but not update the <BrowserRouter>. Instead you can use ConnectedRouter and pass a history instance as
import createHistory from 'history/createBrowserHistory';
...
const history = createHistory();
...
<ConnectedRouter history={history}>
...
</ConnectedRouter>
You need to integrate the react-router-redux.
Its look like the updated state is not reflecting in the container as redux do shallow comparison to check whether the component need to be update or not.
import {Router} from 'react-router-dom';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
const history = createHistory()
const middleware = routerMiddleware(history)
const rootReducer = combineReducers({
router: routerReducer,
//other reducers
});
const store = createStore(rootReducer,applyMiddleware(middleware));
<Provider store={store}>
<ConnectedRouter>
//routes
</ConnectedRouter>
</Provider>
Also,at reducers,add this snippet.
let updatedStore = JSON.parse(JSON.stringify(state));