I have a problem with a react child component, using redux and react-router.
I'm getting a value from the URL through this.props.match.params to make an API call on componentDidMount. I'm using this.props.match.params as I want to use data from the API in my content if someone navigates to this particular URL directly.
When I navigate to the the component by clicking a the component renders fine. The API is called via an action and the reducer dispatches the data to the relevant prop.
When I navigate to the component directly by hitting the URL, the API is called (I can see it called in the Network section of devtools), but the data isn't dispatched to the relevant prop and I don't see the content I expect.
I was hoping that by using this.props.match.params to pass a value to the API call I could hit the component directly and get the data from the API response rendered how I want.
Below is my component code. I suspect something is wrong with my if statement... however it works when I navigate to the component by clicking a Link within my React App.
What Am I missing?
import React from 'react';
import { connect } from 'react-redux';
import { fetchData } from '../../actions';
class Component extends React.Component {
componentDidMount() {
this.props.fetchData(this.props.match.params.id);
}
renderContent() {
if(!this.props.content) {
return (
<article>
<p>Content loading</p>
</article>
)
} else {
return (
<article>
<h2>{this.props.content.title}</h2>
<p>{this.props.content.body}</p>
</article>
)
}
}
render() {
return (
<>
<div className='centered'>
{this.renderContent()}
</div>
</>
)
}
}
const mapStateToProps = (state) => {
return { post: state.content };
}
export default connect(mapStateToProps, { fetchData: fetchData })(Component);
Try updating renderContent to this:
renderContent = () => {
if(!this.props.content) {
return (
<article>
<p>Content loading</p>
</article>
)
} else {
return (
<article>
<h2>{this.props.content.title}</h2>
<p>{this.props.content.body}</p>
</article>
)
}
}
It looks like you forgot to bind this to renderContent
I fixed the problem, huge thanks for your help!
It turned out the API request was returning an array, and my action was dispatching this array to props.content.
This particular API call will always return an array with just one value (according to the documentation it's designed to return a single entry... but for some reason returns it in an array!). I was expecting an object so I was treating it as such.
I converted the array into an object in my reducer and now it's behaving as it should.
So overall turned out to be a problem with my data structure.
Related
so I am new to React. Loving it so far. However, I am having a basic question which doesn't have a clear answer right now.
So, I am learning how to lift the state of a component.
So here's a reproducible example.
index.js
import React from "react";
import ReactDOM from "react-dom"
import {Component} from "react";
// import AppFooter from "./AppFooter";
import AppContent from "./AppContent";
import AppHeader from "./AppHeader";
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min'
import './index.css'
class App extends Component{
constructor(props) {
super(props);
this.handlePostChange = this.handlePostChange.bind(this)
this.state = {
"posts": []
}
}
handlePostChange = (posts) => {
this.setState({
posts: posts
})
}
render() {
const headerProps = {
title: "Hi Keshav. This is REACT.",
subject: "My Subject is Krishna.",
favouriteColor: "blue"
}
return (
<div className="app">
<div>
<AppHeader {...headerProps} posts={this.state.posts} handlePostChange={this.handlePostChange}/>
<AppContent handlePostChange={this.handlePostChange}/>
</div>
</div>
);
}
}
ReactDOM.render(<App/>, document.getElementById("root"))
I am trying to lift the state of posts which is changed in AppContent to AppHeader.
Here's my AppContent.js and AppHeader.js
// AppContent.js
import React, {Component} from "react";
export default class AppContent extends Component{
state = {
posts: []
}
constructor(props) {
super(props); // constructor
this.handlePostChange = this.handlePostChange.bind(this)
}
handlePostChange = (posts) => {
this.props.handlePostChange(posts)
}
fetchList = () => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) =>
response.json()
)
.then(json => {
// let posts = document.getElementById("post-list")
this.setState({
posts: json
})
this.handlePostChange(json)
})
}
clickedAnchor = (id) => {
console.log(`Clicked ${id}`)
}
render() {
return (
<div>
<p>This is the app content.</p>
<button onClick={this.fetchList} className="btn btn-outline-primary">Click</button>
<br/>
<br/>
<hr/>
<ul>
{this.state.posts.map((item) => {
return (
<li id={item.id}>
<a href="#!" onClick={() => this.clickedAnchor(item.id)}>{item.title}</a>
</li>
)
})}
</ul>
<hr/>
<p>There are {this.state.posts.length} entries in the posts.</p>
</div>
)
}
}
// AppHeader.js
import React, {Component, Fragment} from "react";
export default class AppHeader extends Component{
constructor(props) {
super(props); // constructor
this.handlePostChange=this.handlePostChange.bind(this)
}
handlePostChange = (posts) => {
this.props.handlePostChange(posts)
}
render() {
return (
<Fragment>
<div>
<p>There are {this.props.posts.length} posts.</p>
<h1>{this.props.title}</h1>
</div>
</Fragment>
)
}
}
So here's the main question. As we see, that I am calling the dummy posts api and trying to show the titles of the json object list returned by it.
The posts state is actually updated in AppContent and is shared to AppHeader by lifting it to the common ancestor index.js
However, here's what I have observed.
When I keep this code running using npm start I see that anytime I make a change in any place, it refreshes. I was under the impression that it renders the whole page running on localhost:3000.
Say here's my current situation on the web page:
Now, say I make a change in just AppContent.js, then here's how it looks then:
In here, we see that it's still showing 100 posts in case of AppHeader. Is this expected that react only reloads the component and not the whole page. When I refresh the whole page, it shows 0 posts and 0 posts in both the places. Now have I made a mistake in writing the code ? If yes, how do I fix this ?
Thank you.
In case the question is not clear please let me know.
In here, we see that it's still showing 100 posts in case of AppHeader. Is this expected that react only reloads the component and not the whole page.
It's not React, per se, that's doing that. It's whatever you're using to do hot module reloading (probably a bundler of some kind, like Webpack or Vite or Rollup or Parcel or...). This is a very handy feature, but yes, it can cause this kind of confusion.
Now have I made a mistake in writing the code ?
One moderately-signficant one, a relatively minor but important one, and a couple of trivial ones:
posts should either be state in App or AppContent but not both of them. If it's state in both of them, they can get out of sync — as indeed you've seen with the hot module reloading thing. If you want posts to be held in App, fetch it there and provide it to AppContent as a property. (Alternatively you could remove it from App and just have it in AppContent, but then you couldn't show the total number of posts in App.)
When you're rendering the array of posts, you need to have a key on each of the li items so that React can manage the DOM nodes efficiently and correctly.
There's no need to wrap a Fragment around a single element as you are in AppHeader.
If you make handlePostChange an arrow function assigned to a property, there's no reason to bind it in the constructor. (I would make it a method instead, and keep the bind call, but others like to use an arrow function and not bind.)
There's no reason for the wrapper handlePostChange functions that just turn around and call this.props.handlePostChange; just use the function you're given.
Two issues with your fetch call:
You're not checking for HTTP success before calling json. This is a footgun in the fetch API I describe here on my very old anemic blog. Check response.ok before calling response.json.
You're ignoring errors, but should report them (via a .catch handler).
This question already has an answer here:
Problem in redirecting programmatically to a route in react router v6
(1 answer)
Closed last year.
I want to programatically redirect the user to a loading page while I await for a response from the server api. I'm trying to do this inside a class component
The code I've got looks more or less like this:
class App extends Component {
constructor(props){
super(props);
}
handleSubmit = () => {
useNavigate("/loading")
}
}
render() {
return (
<div>
<button onClick={this.handleSubmit}>
Upload!
</button>
</div>
);
}
}
export default App;
The thing is that nothing happens when i click the "Upload!" button. I've read that useNavigate cannot be used inside a class component but I'm not sure how I could implement this differently.
I guess my question is, how can I use useNavigate inside a class component?
EDIT:
Thanks for your responses. I finally decided to convert my code to a function using these steps: https://nimblewebdeveloper.com/blog/convert-react-class-to-function-component
It now works like a charm.
Your clarification is correct, useNavigate() is a hook and therefore can only be used in a functional component. I'm thinking as an alternative you can wrap your App with withRouter, a HOC that gives the wrapping component access to the match, location, and history objects. From there, you can update the location with history.push('/loading').
Please see here for more information on history.
You cannot use useNavigate which is a react hook inside of class component.
you can by the way use react-router-dom which provide different way to manipulate browser url.
Create a functional component as a Wrapper
import { useNavigate } from 'react-router-dom';
export const withRouter = (ClassComponent) => {
const RouterWrapper = (props) => {
const navigate = useNavigate();
return (
<ClassComponent
navigate={navigate}
{...props}
/>
);
};
return RouterWrapper;
};
Then in your App.js, export it by wraping with the functional component
import { withRouter } from './wrapper';
class App extends Component {
constructor(props){
super(props);
}
handleSubmit = () => {
useNavigate("/loading")
}
render() {
return (
<div>
<button onClick={this.handleSubmit}>
Upload!
</button>
</div>
);
}
}
export default withRouter(App);
I'm working on a site where I have a gallery and custom build lightbox. Currently, I'm querying my data with a page query, however, I also use them in other components to display the right images and changing states. It is easier for me to store states in Context API as my data flow both-ways (I need global state) and to avoid props drilling as well.
I've setup my context.provider in gatsby-ssr.js and gatsby-browser.js like this:
const React = require("react");
const { PhotosContextProvider } = require("./src/contexts/photosContext");
exports.wrapRootElement = ({ element }) => {
return <PhotosContextProvider>{element}</PhotosContextProvider>;
};
I've followed official gatsby documentation for wrapping my root component into context provider.
Gallery.js here I fetch my data and set them into global state:
import { usePhotosContext } from "../contexts/photosContext";
const Test = ({ data }) => {
const { contextData, setContextData } = usePhotosContext();
useEffect(() => {
setContextData(data);
}, [data]);
return (
<div>
<h1>hey from test site</h1>
{contextData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
<OtherNestedComponents />
</div>
);
};
export const getData = graphql`
query TestQuery {
allStrapiCategory(sort: { fields: name }) {
allCategories: nodes {
name
}
}
}
`;
export default Test;
NOTE: This is just a test query for simplicity
I've double-checked if I get the data and for typos, and everything works, but the problem occurs when I try to render them out. I get type error undefined. I think it's because it takes a moment to setState so on my first render the contextData array is empty, and after the state is set then the component could render.
Do you have any idea how to work around this or am I missing something? Should I use a different type of query? I'm querying all photos so I don't need to set any variables.
EDIT: I've found a solution for this kinda, basically I check if the data exits and I render my component conditionally.
return testData.length === 0 ? (
<div className="hidden">
<h2>Hey from test</h2>
<p>Nothing to render</p>
</div>
) : (
<div>
<h2>Hey from test</h2>
{testData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
</div>
);
However, I find this hacky, and kinda repetitive as I'd have to use this in every component that I use that data at. So I'm still looking for other solutions.
Passing this [page queried] data to root provider doesn't make a sense [neither in gatsby nor in apollo] - data duplication, not required in all pages/etc.
... this data is fetched at build time then no need to check length/loading/etc
... you can render provider in page component to pass data to child components using context (without props drilling).
I have the following JSON from my test API:
{
"/page-one": "<div><h1 className='subpage'>Database page one!</h1><p className='subpage'>Showing possibility of page one</p><p className='subpage'>More text</p></div>",
"/page-two": "<div><h1 className='subpage'>Database page two!</h1><p className='subpage'>Showing possibility of page two</p><p className='subpage'>More text</p></div>",
"/page-two/hej": "<div><h1 className='subpage'>Database page two supbage hej!</h1><p className='subpage'>Showing possibility of page two with subpage</p><p className='subpage'>More text</p></div>"
}
I want do create a dynamig page that uses the URL pathname to determine which HTML should be used/displayed. I have tried the following:
import React from 'react';
import Utils from "../../utils";
import './not-found-controller.css';
class GenericController extends React.Component {
constructor(props) {
super(props);
this.getPage = this.getPage.bind(this);
}
async getPage() {
const pageList = await Utils.http.get('http://demo5369936.mockable.io/pages');
console.log(pageList.data);
const possiblePages = Object.keys(pageList.data);
if (possiblePages.indexOf(window.location.pathname) !== -1) {
return pageList.data[window.location.pathname];
} else {
return (
<div>
<h1 className="subpage">404 - Page not fpund!</h1>
<p className="subpage">The page you are looking for is not found. Please contact support if you thing this is wrong!</p>
</div>
);
}
}
render() {
return (
<section className="not-found-controller">
{ this.getPage }
</section>
);
}
}
export default GenericController;
But this does not work. I only get the following error:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
in section (at not-found-controller.js:31)
in NotFoundController (at app.js:166)
in Route (at app.js:164)
in Switch (at app.js:88)
in div (at app.js:87)
in App (created by Route)
in Route (created by withRouter(App))
in withRouter(App) (at src/index.js:18)
in Router (created by BrowserRouter)
in BrowserRouter (at src/index.js:17)
The API call is correct, the console.log(pageList.data); prints the JSON correctly. I tried to search for how to include HTML like this but I did not find any answer that made me understand.
3 things wrong here, but your browser's console should've told you the errors already.
First, your getPage method is async, but React rendering passes should be synchronous and execute immediately with whatever data is present. If the data is not present, the component should still render (you can return null from render to render "nothing"). Instead, fetch your data outside of a render pass and call setState when the data is available.
Second, getPage is returning a HTML string, which is not going to be parsed this way, but rendered as-is. You need to use the dangerouslySetInnerHTML prop.
Third, as jitesh pointed out, you are missing the function call brackets.
All things considered, you need something like the following (just thrown this together real quick, but you should get the idea):
constructor(props, context) {
super(props, context);
this.state = {
pageData: undefined
};
}
componentDidMount() {
this.getPage();
}
async getPage() {
const pageList = await Utils.http.get('http://demo5369936.mockable.io/pages');
this.setState({
pageData: pageList.data[window.location.pathname]
});
}
render() {
if (this.state.pageData) {
return (
<section
className="not-found-controller"
dangerouslySetInnerHTML={{ __html: this.state.pageData }}
/>
);
} else {
// Not found or still loading...
}
}
1) Use state to put loaded data to render. Like this.setState({pageHtml: pageList.data})
2) Use dangerouslySetInnerHTML to render your own html https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
I am having a bit of an issue rendering components before the state is set to the data from a returned asynchronous API request. I have a fetch() method that fires off, returns data from an API, and then sets the state to this data. Here is that block of code that handles this:
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi.USD.rate);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
You will notice the console.log('API CALL', currentPrice.bpi.USD.rate) that I use to check if the API data is being returned, and it absolutely is. currentPrice.bpi.USD.rate returns an integer (2345.55 for example) right in the console as expected.
Great, so then I assumed that
this.setState = ({ currentPrice: currentPrice.bpi.USD.rate }) should set the state without an issue, since this data was received back successfully.
So I now render the components like so:
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
With this, I was expecting to be able to access this data in my PriceOverview.js component like so: this.props.data
I have used console.log() to check this.props.data inside my PriceOverview.js component, and I am getting 'null' back as that is the default I set intially. The issue I am having is that the components render before the API fetch has ran it's course and updated the state with the returned data. So when App.js renders the PriceOverview.js component, it only passes currentPrice: null to it, because the asynchronous fetch() has not returned the data prior to rendering.
My confusion lies with this.setState. I have read that React will call render any time this.setState is called. So in my mind, once the fetch() request comes back, it calls this.setState and changes the state to the returned data. This in turn should cause a re-render and the new state data should be available. I would be lying if I didn't say I was confused here. I was assuming that once the fetch() returned, it would update the state with the requested data, and then that would trigger a re-render.
There has to be something obvious that I am missing here, but my inexperience leaves me alone.. cold.. in the dark throws of despair. I don't have an issue working with 'hard coded' data, as I can pass that around just fine without worry of when it returns. For example, if I set the state in App.js to this.state = { currentPrice: [254.55] }, then I can access it in PriceOverview.js via this.props.data with zero issue. It's the async API request that is getting me here, and I am afraid it has gotten the best of me tonight.
Here App.js in full:
import React, { Component } from 'react';
import './components/css/App.css';
import NavigationBar from './components/NavigationBar';
import PriceOverview from './components/PriceOverview';
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
Here is PriceOverview.js in full:
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
currentPrice: this.props.data
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Thank you in advance to any help, it's much appreciated.
this.setState ({
currentPrice: currentPrice.bpi.USD.rate
})
Do not put an = in this.setState
Ok First thing, when you're writting code on React the components that hold state are the class base components so ... What I see here is that you're creating two class base components so when you pass down props from your app class component to your PriceOverview wich is another class base component you're essentially doing nothing... Because when your constructor on your PriceOverview get call you're creating a new state on that Component and the previous state ( that's is the one you want to pass down) is being overwritten and that's why you're seem null when you want to display it. So it should work if you just change your PriveOverview component to a function base component ( or a dumb component). So this way when you pass down the state via props, you're displaying the correct state inside of your div. This is how would look like.
import React from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
const PriceOverview = (data) => {
return (
<div className="overviewBar">
<div className="currentPrice panel">
//Im calling data here because that's the name you gave it as ref
//No need to use 'this.props' you only use that to pass down props
{data != null ? <div className="price">
{data}</div> : <div className="price">Loading...</div>
}
</div>
</div>
)
}
}
export default PriceOverview;
Whenever you're writing new components start always with function base components if you component is just returning markup in it and you need to pass some data go to his parent component update it (making the api calls there or setting the state there) and pass down the props you want to render via ref. Read the React docs as much as you can, hope this explanation was useful (my apologies in advance if you don't understand quite well 'cause of my grammar I've to work on that)
The thing is constructor of any JS class is called only once. It is the render method that is called whenever you call this.setState.
So basically you are setting currentPrice to null for once and all in constructor and then accessing it using state so it will always be null.
Better approch would be using props.
You can do something like this in your PriceOverview.js.
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.props.data!= null ? <div className="price">{this.props.data}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Or you can use react lifecycle method componentWillReceiveProps to update the state of PriceOverview.js
componentWillReceiveProps(nextProps) {
this.setState({
currentPrice:nextProps.data
});
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice }</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}