Manipulating data before rendering in React component - javascript

Coming with an Angular background to React, I can't wrap my head around on how to manipulate and restructure data before rendering it.
Let's say I pass this object from parent component to child:
{
"_id": "5c716c53591610007f6d44ef",
"model": {
"_id": "5c7166eb591610007f6d44d4",
"name": "E 300"
},
"serie": {
"_id": "5c716ba0591610007f6d44e2",
"name": "E-Class"
}
},
{
"_id": "5c716c60591610007f6d44f2",
"model": {
"_id": "5c7166f2591610007f6d44d6",
"name": "E 220"
},
"serie": null
},
{
"_id": "5c716c6a591610007f6d44f3",
"model": {
"_id": "5c7166fe591610007f6d44d8",
"name": "C 180"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
},
{
"_id": "5c716c6e591610007f6d44f4",
"model": {
"_id": "5c716702591610007f6d44d9",
"name": "C 200"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
},
{
"_id": "5c716c74591610007f6d44f5",
"model": {
"_id": "5c716705591610007f6d44da",
"name": "C 220"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
}
I want to categorise each model name under their series.
For example C-Class: ['E300', 'E220'] etc, and also put models with no series defined into an object NoClass.
It's too much for an inline jsx (at least it seems so ??) so I need some helper functions, but I can't manage to do the manipulation because every time I get error that the data is undefined which means it tries to render code before it even appears there and gets modified.
So pretty much I want to filter data into new objects and then render these instead of the original props data. And I don't know how to do that all before rendering
My current attempt at doing it inline - it does render however it doesn't check for empty series objects and breaks when where are more than 1 different serie:
class ModelSelect extends React.Component {
render() {
const { models } = this.props
return (
models && <div className={"ModelSelect_Wrapper"}>
<Select placeholder={"Model"} loading={models.loading} disabled={models.data.length === 0}>
{_.uniqBy(models.data, 'name').map((serie) =>
<OptGroup label={serie.serie.name}>
{!models.loading && models.data.map((model, index) =>
<Option value={model.model._id} key={index}>{model.model.name}</Option>
)}
</OptGroup>
)}
</Select>
</div>
);
}

The easiest way to do this is using stores. You have several options available, such as Redux, Flux, and Mobx. Unfortunately, learning to use Stores is a bit hard. coming from angular, i would compare a store to $scope from angular.js 1x, with the difference being that there is no magic causing the view to rerender, you have to rely on react detecting a change in the props in order for rerender to occur. the idea is that the store is a data structure, and when it is modified, the components which are using the store as a prop will be rerendered. this works great especially if you are fetching data async.
the basic pattern would be
use componentDidMount() to load the data in Higher Order Component
transform the data and update the store with the new data
you are passing the store to your component like <MyComponent store={store} />
when data is loaded, you pass the store to the child component
doing it this way your component may look something like
class MyComponent extends React.Component {
static defaultProps = {
store:{}
}
state = {
loading: true
}
componentDidMount() {
this.props.store.loadSomeData().then(()=>this.setState({loading:false}));
}
render(){
return this.state.loading ? null : <MyChildComponent store={store} />
}
}
please remember, you are responsible for loading and transforming the data in your store, and this is just a generalized overview of how your component show load and receive that transformed data. your exact approach will vary wildly based upon the store you are using.
My answer can't be any more specific than this, because there is far too much to cover, and the scope of the work required varies highly depending on the library you use. For example, mobx-3x all you do is decorate your component with #inject and #observer and it handles the shouldComponentUpdate() implementation for you.
So your, first step is to pick a store, and then go from there. best of luck.

Related

Component not changing after state change (componentDidMount fetch) - React

I'm making an news app following a tutorial I'm fetching data from newapi my code looks same as tutorial but my component does not change after I update the state (this.state.articles) I'm using setState function i tried console logging the state it looks fine after the state is updated render methods runs but it does not change anything what could be worng
my code/component
import React, { Component } from 'react'
import NewsItem from './NewsItem'
export default class News extends Component {
articles = [
{
"source": {
"id": "espn-cric-info",
"name": "ESPN Cric Info"
},
"author": null,
"title": "PCB hands Umar Akmal three-year ban from all cricket | ESPNcricinfo.com",
"description": "Penalty after the batsman pleaded guilty to not reporting corrupt approaches | ESPNcricinfo.com",
"url": "http://www.espncricinfo.com/story/_/id/29103103/pcb-hands-umar-akmal-three-year-ban-all-cricket",
"urlToImage": "https://a4.espncdn.com/combiner/i?img=%2Fi%2Fcricket%2Fcricinfo%2F1099495_800x450.jpg",
"publishedAt": "2020-04-27T11:41:47Z",
"content": "Umar Akmal's troubled cricket career has hit its biggest roadblock yet, with the PCB handing him a ban from all representative cricket for three years after he pleaded guilty of failing to report det… [+1506 chars]"
},
{
"source": {
"id": "espn-cric-info",
"name": "ESPN Cric Info"
},
"author": null,
"title": "What we learned from watching the 1992 World Cup final in full again | ESPNcricinfo.com",
"description": "Wides, lbw calls, swing - plenty of things were different in white-ball cricket back then | ESPNcricinfo.com",
"url": "http://www.espncricinfo.com/story/_/id/28970907/learned-watching-1992-world-cup-final-full-again",
"urlToImage": "https://a4.espncdn.com/combiner/i?img=%2Fi%2Fcricket%2Fcricinfo%2F1219926_1296x729.jpg",
"publishedAt": "2020-03-30T15:26:05Z",
"content": "Last week, we at ESPNcricinfo did something we have been thinking of doing for eight years now: pretend-live ball-by-ball commentary for a classic cricket match. We knew the result, yes, but we tried… [+6823 chars]"
}
]
constructor() {
super();
this.state = {
articles: this.articles,
loading: false
}
}
async componentDidMount() {
const URL = "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey="
let data = await fetch(URL);
let parsedData = await data.json()
this.setState({
articles: parsedData.articles
})
console.log(this.state.articles)
}
render() {
console.log("render")
return (
<div>
<div className="container">
<h2 className='my-4 mx-4'> NewsMonkey - Top Headlines </h2>
<div className="row">
{this.articles.map((elem) => {
return <div className="col-md-4" key={elem.url}>
<NewsItem title={elem.title?elem.title.slice(42):""} desc={elem.description?elem.description.slice(0, 88): ""} url={elem.url} imgURL={elem.urlToImage} />
</div>
})}
</div>
</div>
</div>
)
}
}
this.articles and this.state.articles are not the same.
You have a static property this.articles that you are using in the render logic - this.articles.map(.... Your fetch is updating state (like it should be).
Update your render to read from this.state.articles and it should work.
Hi #Curious Your code is correct
you just need to pay attention when making the map
you are using this.articles which is a fixed (mock) list
you need to call map in this.state.articles because this is the state you change in didMount

Looping through axios API returns undefined

I'm pulling an object out of an API that I'm using to show data on a website.
Here's the object:
{
"id": "25ce5d91-a262-4dcf-bb87-42b87546bcfa",
"title": "Les Houches The Hidden Gem Of The Chamonix",
"channel": "Cornelia Blair",
"image": "https://i.imgur.com/yFS8EBr.jpg",
"description": "Les Houches, located 6 kilometres from Chamonix, is a ski resort with a domain which extends from an altitude of 950 metres up to 1900 metres. Long descents through tree-lined slopes are combined with impressive views of the Mont Blanc massif and the Chamonix valley. Les Houches is twinned with the Russian villages of Sochi and Krasnaya-Polyana and was chosen by the International Olympic Committee to assist in the organization of the 2014 Winter Olympic Games. Watch for more fun facts!",
"views": "16,950",
"likes": "3,856",
"duration": "4:13",
"video": "https://project-2-api.herokuapp.com/stream",
"timestamp": 1622991672000,
"comments": [
{
"id": "7ba106bf-e74a-4c21-b59e-c485a30eea45",
"name": "Giovana Alpine",
"comment": "Wow! You can bet that we’ll be checking this place out when we’re in the area. The views look absolutely breathtaking. Thank you so much for sharing this with the world!",
"likes": 0,
"timestamp": 1623104591000
},
{
"id": "921f0e8d-f9d1-44db-b4a2-a2718339891e",
"name": "Victoire Lesage",
"comment": "Skiing is a lifestyle! This may be hard to believe now, but I once competed here for the World Cup. The Alps are at their most beautiful when you’re shredding down them FAST.",
"likes": 0,
"timestamp": 1623071160000
},
{
"id": "f7b9027b-e407-45fa-98f3-7d8a308ddf7c",
"name": "Sharon Tillson",
"comment": "Amazing footage of an amazing spot! It’s so inspiring to watch the sun rising over a landscape like this. I can only imagine how fresh the air must feel there on a snowy morning.",
"likes": 3,
"timestamp": 1623002522000
}
]
}
I'm then using this data and pass it down to others components, i.e. FeaturedVideo, FeaturedVideoDescription as it can be seen in the app.js, which is working just fine.
function VideoPage() {
const [details, setDetails] = useState([]);
const { videoId } = useParams();
useEffect(() => {
axios
.get(
`https://project-2-api.herokuapp.com/videos/${videoId}?api_key=removed for privacy`
)
.then((resp) => setDetails(resp.data));
}, []);
return (
<>
<FeaturedVideo selectedVideo={details} />
<FeaturedVideoDescription selectedVideo={details} />
<div className="information">
<div>
<CommentList selectedVideo={details} />
</div>
</div>
</>
);
}
However, when I then pass it down to the component: CommentList component it stops working. It's giving me the following error "cannot read properties of undefined (reading 'map')". Meaning, there seems to be an issue inside the CommentList component where I'm mapping through the data.
function CommentList({ selectedVideo }) {
return (
<>
<section className="comments">
{selectedVideo.comments.map((comment) => {
return (
<Comment
name={comment.name}
timestamp={comment.timestamp}
comment={comment.comment}
key={uniqid()}
/>
);
})}
</section>
</>
);
}
As it's undefined it means that it's not pulling the data from the API properly? But why is that?
I've also imported axios, useEffect and all the other things that are needed to run this code but decided to not include it in the code snippet here.
{selectedVideo?.comments?.map((comment) => {
You got an error because before to get your data and call the setState function, it is actually undefined so you must wait until it is available and the question mark will help you.
You can also check is details is an empty array or not and decide to render your component or not like this:
if(details.length > 0) return <CommentList selectedVideo={details} />
By the first render selectedVideo.comments is still undefined i.e., during initial render the Comment has comment prop with []
a small e.g.
const data = [];
console.log(data.some_prop.map(()=>{}))
so is the error ... you can use optional chaining one of few ways 😛
const data = [];
console.log(data?.some_prop?.map(()=>{}))
as selectedVideo?.comments?.map()... - no error here because it returns undefined as soon as the property doesn't exist as mentioned in docs link shared above.
after the first render, the component function gets called with new data, in which you have the data set earlier using setDetails ..

How can I put a JSON object into JSX?

I have a json array that contains some information :
[
{
"id": 1,
"imageUrl": "./img1.png",
},
{
"id": 2,
"imageUrl": "./img2.png",
}
]
I tried to make a class in which the elements were sorted and added the necessary link to the src, but the information from JSON is not put into JSX.
class Paintings extends Component {
render(){
return(
<ul>
{
paintings.map((s) => {
return (
<div>
<img src={s.imageUrl} alt={s.id} />
</div>
)
})
}
</ul>
)
The question is more general then just about images here. It is to import and loop a JSON object in JSX.
You have some syntax error in your code, I fixed them and shared an online editor implementation for you to have a look as well.
Save your json in a file and import that file in your react code then map the json object.
For the images location, although this is not the recommended method, I think you should put your images under public folder and make sure you have your json file with the correct path.
Sample JSON file I have structured, for your reference:
[
{
"id": 1,
"imageUrl": "image1.png"
},
{
"id": 2,
"imageUrl": "image2.png"
}
]
Then you should be able to use below code to be able to generate it.
import React, { Component } from "react";
import Painting from "./Painting";
class Paintings extends Component {
render() {
return (
<ul>
{Painting.map(({ id, imageUrl }) => (
<div key={id}>
<img src={imageUrl} alt={id} />
</div>
))}
</ul>
);
}
}
export default Paintings;
Here is a basic implementation for you to check out, we are able to map and see the items. I have added some sample images and please pay attention to json file and how its ready to map the JSX image objects based on the file paths.
https://codesandbox.io/s/react-json-into-jsx-19gj1w

Reveal.js not initialising in vue except on refresh

I'm creating a Vue app to read comic books via Reveal.js. The component takes the data from the parent. there is an Axios call in the parent to provide the data from a rest API. I'm also using Vue router with the createWebHashHistory setup as I'm using a Django backend to provide the API.
If I refresh the page it will load the presentation correctly but when I navigate to the page it doesn't seem to initialise Reveal. there are no errors in the console.
I've tried to watch the route changing and other events to run Reveals sync or initialise but I've not had any success.
component
<template>
<div class="reveal" id="comic_box" ref="comic_box">
<div id="slides_div" class="slides">
<section v-for="(page, index) in comic_data.pages" :key="page.index" :data-menu-title="page.page_file_name">
<img :data-src="'/image/' + comic_data.selector + '/' + page.index " class="w-100" :alt="page.page_file_name">
</section>
</div>
</div>
</template>
<script>
import Reveal from "reveal.js";
export default {
name: "TheComicReader",
data () {
return {
}
},
props: {
comic_data: Object
},
methods: {
},
watch: {
'$route' (to, from) {
Reveal.initialize()
}
},
mounted () {
Reveal.initialize()
},
}
</script>
<style scoped>
</style>
comic_data
{
"selector": "e1b76b93-814c-4ee8-9104-8c8187977836",
"title": "Batman 125 (2022) (digital-SD).cbr",
"last_read_page": 0,
"pages": [
{
"index": 0,
"page_file_name": "Batman 125-000.jpg",
"content_type": "image/jpeg"
},
{
"index": 1,
"page_file_name": "Batman 125-001.jpg",
"content_type": "image/jpeg"
}
]
}
After further investigation I noticed that the DOM elements in Reveal were not updating after moving away from the page. I solved this by forcing Reveal to bind to the new comic_box by ref. This is now consistently loading the presentation correctly.
mounted () {
Reveal(this.$refs.comic_box).initialize()
}

Is there a way to tell Svelte not to recycle a component?

I don't know if the title is super accurate, couldn't think of a better way to explain it.
Let's say we have a model:
let model = [
{
"id": 0,
"title": "Apple"
},
{
"id": 1,
"title": "Orange"
},
{
"id": 2,
"title": "Tomato"
},
{
"id": 3,
"title": "Avocado"
}
]
That we render like this:
{#each model as item}
<Item {...item}></Item>
{/each}
In the Item component we print to console when the component gets mounted and destroyed:
<script>
import {onMount, onDestroy} from 'svelte'
export let id
export let title
onMount(() => {
console.log('Mounted: ' + title)
})
onDestroy(() => {
console.log('Destroyed: ' + title)
})
</script>
<div id={id}>{title}</div>
We also have a button, which deletes the "Orange" element from the model:
function deleteOrange() {
model.splice(1, 1)
model = model // I don't know if this is the correct way to force an update
}
// ...
<input on:click={deleteOrange} type="button" value="Delete Orange">
When the button is pressed, the Orange component disappears from the DOM as it should, but the console says:
"Destroyed: Avocado"
Is there a way to tell Svelte to destroy the "Orange" component, instead of destroying "Avocado" and moving props around to match the model?
Svelte REPL:
https://svelte.dev/repl/f22b37cb88c449118ff76bb5a82c296e?version=3.48.0
Simply add a (key) to your each loop:
{#each model as item (item.id)}
<Item {...item}></Item>
{/each}
This will ensure that DOM elements are properly tracked. As per the Svelte documentation:
If a key expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.

Categories