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
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).
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.
I have a simple component that fetches data and only then displays it:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetch( { path: '/load/stuff' } ).then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
Each instance of MyComponent loads the same data from the same URL and I need to somehow store it to avoid duplicate requests to the server.
For example, if I have 10 MyComponent on page - there should be just one request (1 fetch).
My question is what's the correct way to store such data? Should I use static variable? Or I need to use two different components?
Thanks for advice!
For people trying to figure it out using functional component.
If you only want to fetch the data on mount then you can add an empty array as attribute to useEffect
So it would be :
useEffect( () => { yourFetch and set }, []) //Empty array for deps.
You should rather consider using state management library like redux, where you can store all the application state and the components who need data can subscribe to. You can call fetch just one time maybe in the root component of the app and all 10 instances of your component can subscribe to state.
If you want to avoid using redux or some kind of state management library, you can import a file which does the fetching for you. Something along these lines. Essentially the cache is stored within the fetcher.js file. When you import the file, it's not actually imported as separate code every time, so the cache variable is consistent between imports. On the first request, the cache is set to the Promise; on followup requests the Promise is just returned.
// fetcher.js
let cache = null;
export default function makeRequest() {
if (!cache) {
cache = fetch({
path: '/load/stuff'
});
}
return cache;
}
// index.js
import fetcher from './fetcher.js';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetcher().then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
You can use something like the following code to join active requests into one promise:
const f = (cache) => (o) => {
const cached = cache.get(o.path);
if (cached) {
return cached;
}
const p = fetch(o.path).then((result) => {
cache.delete(o.path);
return result;
});
cache.set(o.path, p);
return p;
};
export default f(new Map());//use Map as caching
If you want to simulate the single fetch call with using react only. Then You can use Provider Consumer API from react context API. There you can make only one api call in provider and can use the data in your components.
const YourContext = React.createContext({});//instead of blacnk object you can have array also depending on your data type of response
const { Provider, Consumer } = YourContext
class ProviderComponent extends React.Component {
componentDidMount() {
//make your api call here and and set the value in state
fetch("your/url").then((res) => {
this.setState({
value: res,
})
})
}
render() {
<Provider value={this.state.value}>
{this.props.children}
</Provider>
}
}
export {
Provider,
Consumer,
}
At some top level you can wrap your Page component inside Provider. Like this
<Provider>
<YourParentComponent />
</Provider>
In your components where you want to use your data. You can something like this kind of setup
import { Consumer } from "path to the file having definition of provider and consumer"
<Consumer>
{stuff => <SomeControl>
{ stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
}
</Consumer>
The more convenient way is to use some kind of state manager like redux or mobx. You can explore those options also. You can read about Contexts here
link to context react website
Note: This is psuedo code. for exact implementation , refer the link
mentioned above
If your use case suggests that you may have 10 of these components on the page, then I think your second option is the answer - two components. One component for fetching data and rendering children based on the data, and the second component to receive data and render it.
This is the basis for “smart” and “dumb” components. Smart components know how to fetch data and perform operations with those data, while dumb components simply render data given to them. It seems to me that the component you’ve specified above is too smart for its own good.
I'm new to ReactJS and I'm unsure if I'm simply doing this wrong (I'm sure I am to a certain extent!) or I just need to change something.
So Basically I'm pulling in some JSON from a Drupal web service. My content pages are basically just the same page, a header and some paragraph text. So I thought the best way to deal with this was to have one component that fetched and displayed the page which changes by the ID fed into the url.
I'm using React Router Dom (v4), when I reload the page it gets the correct page with the correct ID but if I click on the navigation menu it will only ever show the first page you clicked on, but does update the route in the address bar.
I am using Fetch API and the call is made in componentDidMount, I've googled it a lot and it seemed I need to do something with componentWillReceiveProps as componentDidMount is doing what it's supposed to do and only update when the component is first mounted on the page. But I can't figure out how to use componentWillReceiveProps correctly. I kind of have it working (but not really) if I literally put the same function in both componentWillReceiveProps and componentDidMount but that seems wrong to me... This is my component:
import React from 'react';
var urlForSimplePage = id =>
`page-url/${id}`
class SimplePage extends React.Component {
constructor(props) {
super(props)
this.state = {
requestFailed: false,
open: true,
}
}
fetchSimplePage() {
fetch(urlForSimplePage(this.props.match.params.id))
.then(response => {
if (!response.ok) {
throw Error("Network request failed")
}
return response
})
.then(d => d.json())
.then(d => {
this.setState({
SimplePageData: d
})
}, () => {
this.setState({
requestFailed: true
})
})
}
componentDidMount() {
this.fetchSimplePage();
}
componentWillReceiveProps(nextProps) {
this.fetchSimplePage();
}
render() {
if (this.state.requestFailed) return <p>Failed!</p>
if (!this.state.SimplePageData) return <p>Loading...</p>
return (
<div>
<h1>
{this.state.SimplePageData[0].title[0].value}
</h1>
<div dangerouslySetInnerHTML={ {__html: this.state.SimplePageData[0].body[0].value} } />
</div>
)
}
}
export default SimplePage;
I receive the "Uncaught TypeError: Cannot read property 'data' of null" error in the console when I'm loading up my page.
In the constructor I call an external api for data with axios using redux-promise, then I pass props down to a stateless component (TranslationDetailBox).
The data arrives, and the child component will receive the props shortly (page looks ok), but first the error message appears.
this.props.translation is empty before the api call, and rendering happens before the state is updated with the external data. I believe this is the issue, but I have no clue on how to solve it.
class TranslationDetail extends Component {
constructor(props){
super(props);
this.props.fetchTrans(this.props.params.id);
}
render() {
return (
<div className="container">
<TranslationDetailBox text={this.props.translation.data.tech}/>
<TranslationDetailBox text={this.props.translation.data.en}/>
</div>
);
}
}
function mapState(state) {
const { translation } = state;
return { translation };
}
...
I find this to be a good use case for conditional rendering.
Your render could check to see whether the data has loaded or not and if not, render something indicating it is still loading.
A simple implementation could be:
render() {
return (
!this.props.data ?
<div>Loading...</div>
:
<div>{this.props.data}</div>
)
}
You could set defaultProps:
TranslationDetail.defaultProps = {
translation: {
data: {}
}
};