useState and mapping - javascript

I am currently building a "Quote Builder" in React and using the WPAPI to hook into the data within WordPress. This uses ACF to gather further data - which I have done (screenshot attached).
I then have an onChange function which grabs the data (from the screenshot), which will then need to update the state:
const quoteTypeChange = async (e) => {
e.preventDefault();
const optionValue = e.target.value;
try {
await wp.quoteType().id(optionValue).then((data) => {
const quoteTypeDetails = data;
// useState update here //
}).catch((error) => {
// Error //
});
} catch (error) {
// Error //
}
}
Within the "Quote Builder" it will display the data into a table - which I have build the front-end of it and its using the following components:
<QuotePhaseTitle title="Title goes here" style="primary" />
and:
<QuoteComponentRow inclusion="Inclusion goes here" deDesktop="2" deMobile="2" deWireframe="2" digital="2" notes="Test notes" />
What I want to be able to do is using the data from the screenshot, map out the data and structure it using those components. From the data the "phase" element will use "QuotePhaseTitle" component and the "quote" will use the "QuoteComponentRow" component, but those can exist in any order and repeated however often that is needed - but they have to go in the order that appears in the data flow.
How would you go about doing this?
Data

It sounds like the key bits of information you need are that
React elements are essentially JavaScript objects. So <QuotePhaseTitle title="Title goes here" style="primary" /> can be thought of as compiling to something like {elementType: 'QuotePhaseTitle', props: { title: "Title goes here", ... }}. (It's not exactly that, but you can think of it like that.)
React can render arrays of those elements
So if you want a list of elements in your component based on data, you can use standard JavaScript techniques to create that array of React elements and React will render it.
For your data structure, you probably need two nested loops like this:
const quoteList = []
data.acf.phase.forEach(phase => {
quoteList.push(<QuotePhaseTitle title={phase.phase} style="primary" />)
phase.quote.forEach(quote => {
quoteList.push(<QuoteComponentRow inclusion={quote.inclusion} ... />)
})
})
You can create this array first and assign it to state or assign your data to state and then have this code run before returning the JSX.
To render your quotelist, you can just include the array of elements as a child in the JSX.
return (
<div>
{quoteList}
</div>
)

Related

React Query // How to properly use client-state and server-state (when user can drag/sort items on the UI)?

I have an React App using React Query to load a dataset of folders containing items. The app allows the user to drag/sort items (within a folder and drag to another folder). Before implementing drag/sort it was simple, RQ fetched the dataset folderData and then the parent supplied data to child components: parent > folder > item .
In order to implement drag/sort, I am now having to copy the entire dataset folderData into a client-state variable foldersLocal. This is the only way I could figure out how to change the UI when the user moves an item.
However, I feel like this approach essentially removes most of the benefits of using React Query because as soon as the data is fetched, I copy the entire dataset into a "client-side" state variable (foldersLocal) and only work with it. This also makes the QueryClientProvider functionality effectively useless.
Is there a better way to approach this? Or am I missing something?
// Fetch data using React Query
const {isLoading, isError, foldersData, error, status} = useQuery(['folders', id], () => fetchFoldersWithItems(id));
// Place to hold client-state so I can modify the UI when user starts to drag/sort items
const [foldersLocal, setFoldersLocal] = useState(null);
// Store React Query data in client-state everytime it's fetched
useEffect(() => {
if (status === 'success') {
setFoldersLocal(foldersData);
}
}, [foldersData]);
// Callback to change "foldersLocal" when user is dragging an item
const moveItem = useCallback((itemId, dragIndex, hoverIndex, targetFolderId) => {
setFoldersLocal((prevData) => {
// change data, etc. so the UI changes
}
}
// Drastically simplified render method
return (
<QueryClientProvider client={queryClient}>
<FolderContainer folders={foldersLocal} moveItem={moveItem} moveItemCompleted={moveItemCompleted}/>
</QueryClientProvider>
)

React - How to add a component to a string

I currently have a local React website (first project, so learning as I go) which has a select field that is pulling the options in from a database. That bit is fine. When I create a click function "onChange" to then get data from the database, this works fine.
My issue is that I want to be able to grab the data from the JSON data and append the data into a component. I currently have the following component set up, which works when I add this onto the page manually:
<QuotePhaseTitle title="Test Title" style="primary" />
So what I basically want to do is within the "onChange" function, get the data (which I can do easily enough) and then pass that to the "title" and "style" props. Once that has been passed, I then need to be able to return that data and input into the page somewhere.
Below is an example of the function so far (I am using WPAPI):
const quoteTypeChange = async (e) => {
e.preventDefault();
const optionValue = e.target.value;
try {
await wp.quoteType().id(optionValue).then((data) => {
const quoteTypeDetails = data;
// Ideall want to pass in the <QuotePhaseTitle title="Test Title" style="primary" /> component, add in the data and then display that on the page //
}).catch((error) => {
// Error //
});
} catch (error) {
// Error //
}
}
How would I go about doing this? Sorry if this is a basic question.
The code itself doesn't "pass values to components", it doesn't really interact with components at all in general. The code updates state. The components use state. So your component might define two state values, for example:
const [title, setTitle] = useState('Test Title');
const [style, setStyle] = useState('primary');
And you would use that state in the JSX:
<QuotePhaseTitle title={title} style={style} />
Then all you need to do is update the state values:
wp.quoteType().id(optionValue).then((data) => {
setTitle(data.someValue);
setStyle(data.someOtherValue);
})
Structurally this is fundamental to React. State drives display, logic updates state.
You need to create a state, so when the data comes from server, you put them on the state for example this.setState({ title: data.title }) o using hooks const [title, setTitle] = useState(); setState(data.title);
And then pass the title value to your component: <QuotePhaseTitle title={this.state.title} style="primary" /> of <QuotePhaseTitle title={title} style="primary" /> if you are using hooks.
Also you can instantiate the hook value or the state with a default value.

Returning a nested JSON object to front end as text in React

I'm successfully fetching from an API but having trouble rendering the data I want to the front end in React.
I'm trying to return the entire contents of the 'model' object within Article component which is a set of key/value pairs. The ArticleList component maps the body key representing an array and the type and model are passed as props to the Article component.
The JSON of the mock API being accessed is here for reference of the structure: https://www.mocky.io/v2/5c6574b33300009010b99de4
I can't use map on the inner objects because they are not arrays. The console.log in my code is correctly returning the contents of the model object for each entry in the array within the inspection window. However, I can't get it to display in the browser view.
In the Article component the use of Object.toString(model) is temporary code I added in to allow my browser window to render and it displays the following in the browser view:
Object: function Object() { [native code] }
So, to be clear my app is returning the full model object to the browser and not the contents within as desired.
ArticleList component is:
import Article from './Article';
const ArticleList = ({article}) => {
const articleNodes = article.body.map((section)=>{
return (
<>
<Article type={section.type} model={section.model} />
</>
)
})
return (
<>
{articleNodes}
</>
);
}
export default ArticleList;
Article component which is receiving the props of type and model is:
function Article({type, model}) {
console.log(model);
if (!type) return null;
return(
<>
<h1>type: {type}</h1>
<h1>Object: {Object.toString(model)}</h1>
</>
);
}
export default Article;
Please advise? I believe the fix could be simple within my <h1> tag of Article component but I have tried to no avail.
Edit: I want to be able to semantically tag each key value pair to render the images within the "url" key within img tags etc. Therefore I ideally need to have the ability to return individual JSX elements representing the properties in the object such as I have done with the map in ArticleList.
A component for every type
You'll need to create multiple components for each type of data you're receiving. That means a component for the heading type, for the paragraph type, etc.
const Paragraph = ({ text }) => (
<p>{text}</p>
);
export default Paragraph
Finding the right component for the right type.
Now that you've created multiple components for each type your API returns, you'll need to find a way to select the component that corresponds with the current type value.
An easy way to do this is to create a map object. The keys will represent the possible types and the values a function that return the component.
const typesMap = {
'paragraph': (props) => <Paragraph {...props}/>
}
The example above shows a single option called paragraph. (Other types heading, image, etc. are yours to add.) The value is a function with a single parameter called props. The function returns a Paragraph component and passes all available props to the component with the ... spread syntax.
The spread syntax allows us not to hardcode our props like the example below,
<Paragraph text={text} someprop={somevalue} />
but passes all the properties and values in the props object to the component, saving us effort and time.
Selecting the component
We have an object of keys that represent types and values that represent the components. All you have to do now is to select the component based on the type value. And we can do that like this:
const TypeComponent = typesMap[type];
Remember, the selected value is a function that returns a component, like <Paragraph/>. Which would be the same as something like this:
const ExampleComponent = ({ props }) => (
<Paragraph {...props}/>
);
In React (functional) components are nothing more than actual functions that return values. That's why we can write TypeComponent like a component and call it like this:
<TypeComponent {...props} />
The name TypeComponent can be anything you'd like. I thought that it was appropriate for the context it is in.
Now all you have to do is pass the model object to the dynamically created TypeComponent with the spread syntax, saving you the trouble of writing all the props and values for each type.
import Paragraph from './Paragraph';
const typesMap = {
'paragraph': props => <Paragraph {...props} />,
//'heading': props => ...
//'image': ...
};
function Article({ type, model }) {
if (!type) return null;
const TypeComponent = typesMap[type];
return <TypeComponent {...model} />;
}
export default Article;

React setState late updation with API calls [duplicate]

This question already has answers here:
if-else statement inside jsx: ReactJS
(17 answers)
Closed 2 years ago.
I am new to React and as I was creating a project, I came across a rather peculiar event with my code.
It is with the async nature of the setState which doesn't let me render data on my page like i want it to.
I am trying to display files which i have in my database already, onto my webpage. But since the state is set after a while therefore i am not able to render my files onto the screen using the map function, since when it is called it is undefined.
I implplemented a get method in so as to get the json response of the files that i want to display. But as I mount my component and setstate of the Files, it shows that it doesn't have any value in it.I know it is async but i have no idea how to handle it so that i can use map to display onto the webpage.
My code is as follows:
import React, {Component} from "react";
// import axios from 'axios';
import {Link} from 'react-router-dom';
import {Styles} from '../Styling/Styles';
class FileView extends Component {
state = {
fileViewData : {}
}
// viewFunction(){
componentDidMount(){
fetch('http://127.0.0.1:8000/api/uploads/', {
method: 'GET'
})
.then((response) => {
let data = response.json();
return data;
})
.then((data) => {
this.setState({fileViewData: data}, ()=>{
console.log("hi");
});
}).catch(error => {console.log(error)})
// console.log(fileViewData);
}
render(){
return (
<React.Fragment>
<Styles>
<div className = "appbar">
<Link to='/dashboard'>
<button className="homeBtn" label="Back" >
Back
</button>
</Link>
</div>
{/* <button label="View" onClick={this.viewFunction} >
View
</button> */}
</Styles>
//.....section not working since map is not a function of undef......//
{
this.state.fileViewData.map(item =>{
return (
<h2>{item.fields.file}</h2>
);
})
}
//.......section not working ends.........//
{console.log(this.state.fileViewData)}
</React.Fragment>
);
}
}
export default FileView;
The console output is something like this:
The empty object is returned twice and then the data is returned twice.I am not running any kind of loop either.
How should I set the value so that i am able to display my files onto the screen? TIA!
Looks like your data is an array, so your initial state should also be an array
state = {
fileViewData: [],
}
Then your check for array length will be correct, but regular javascript doens't quite work the same in JSX. You'll need to conditionally render JSX.
Conditional Rendering
{this.state.fileViewData.length
? this.state.fileViewData.map(...)
: null
}
Since it seems you don't really render anything if there is no data, you can simply map the data as array::map correctly handles empty arrays.
{
this.state.fileViewData.map(...)
}
Set state to an empty array
Remove the if condition and anonymous function declaration from your map statement
You declare that function but never invoke it also you don't need it.
if you insist on checking the length
{
this.state.fileViewData.length &&
this.state.fileViewData.map(item =>{
return (
<h2>{item.fields.file}</h2>
);
})
}
You are getting multiple console logs because when you set state in React you cause the component to render again.
As for your implementation, you probably want the initial value of fileViewData to have the same data structure as what your back end hands back. Currently you start with a plain javascript object, and then turn it into an array of objects once the response comes from your back end.
Without any consideration for a loading period, a simple thing to do to make your application not crash until data loads in would be to make fileViewData an empty array when it is initialized, not an object with no keys. Then it would have the correct prototype to have the method map.

Dynamically replace elements using jsx

I'm having an array data.info that is being updated over time and I'm trying to replace placeholder rendered elements with another. So by default app.js looks like this
return (
<Fragment>
{data.info.map((index) => {
return <Typography key={index} variant="h6" className={classes.title}>Demo</Typography>
})}
</Fragment>
)
Also I have a hook with async function to subscribed to data.info.length.
useEffect(
() => {
if (!initialRender.current) {
if (data.info.length!==0) {
for (let i = data.info.length-iScrollAmount+1 ; i < data.info.length+1; i++) {
firstAsync(i)
}
}
} else {
initialRender.current = false
}
},
[data.info.length]
)
async function firstAsync(id) {
let promise = new Promise(() => {
setTimeout(() => console.log(document.getElementById(id)), 500)
});
}
With document.getElementById() and id I can get to every element that was rendered and change it. And here goes the problems.
I'm using material-ui so I can't get to <Typography/> because it is transformed into <h6/>. Probably that is not a problem since I need to replace contents, so I can find parent element and remove all children. Is that way correct?
After I delete children how do I add content using jsx? What I mean is that in async function I'll get an array that I want to use in new element <NewCard/> to dynamically put into <Fragment/>. Yet I did not find any example how to do that.
It is not a good practice to change DOM Nodes directly in React, and you need to let React do the rendering for you and you just tell react what to do.
in your case you need to define a React State for your data and set your state inside your firstAsync function and then use your state to render whatever html element or React component which you want
React does not encourage the practice of manipulating the HTML DOM nodes directly.
Basically you need to see 2 things.
State which is a special variable whose value is retained on subsequent refresh. Change in reference in this variable will trigger component and its children a refresh/re-render.
Props which is passed to every Component and is read only. Changing in props causes refresh of component by default.
In your example, based on data.info you want to render Typography component.
Solution
First thing is your map function is incorrect. First parameter of map function is item of list and second is index. If you are not sure if info will always be present in data, you may want to have a null check as well.
{(data.info || []).map((info, index) => {
return <Typography key={index} variant="h6" className={classes.title}>{info.text}</Typography>
})}
You should be passing info from map to Typography component. Or use info value in content of Typography as shown above.
Update data.info and Typography will update automatically. For this, please make sure, data.info is a component state and not a plain variable. Something like
const [data, setData] = React.useState({});
And when you have value of data (assuming from API), then
setData(responseApi);

Categories

Resources