React - How to add a component to a string - javascript

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.

Related

how to get react function to update state with user input

I have written a function in react that, through a rendered submit form, takes a user's input, uses it to query an api and updates p tag content in the form. This is the following code:
var textfield = ""
function Form (){
const [ name, getNFT ] = useState("")
let postName = (e) =>{
let output
async function getInfo(e) {
e.preventDefault()
try {
const resp = await axios.post("/hey_honey", {
name
})
console.log(resp.data)
output = resp.data
return output
} catch (error) {
console.error(error)
}
}
getInfo(e).then(output =>{console.log("output is outside of function scope", output)
})
textfield = output
return textfield
}
return (
<div className="App">
<form onSubmit={postName}>
<input type="text" value={name} onChange={(e) => getNFT(e.target.value)}/>
<button type="submit" >Get Collection's Gallery</button>
<p>{textfield}</p>
</form>
</div>
)
}
I have successfully gotten the JSON string that I wanted. The problem is I seemingly have no way of updating the rendered form because it is either outside of postName's function scope or anything modified by postName even if instantiated outside of form cannot actually change the states.
At one point I thought that this was most likely because this is a more appropriate instance of using a class, I however can't use useState and therefore would have no way of storing the content written out in the submit form. What am I missing here?"
(Edit: after some research there are ways I can do this with a class. I technically don't need to utilize useState for a form but rather I could use setState and simply instantiate setState as the empty string.
It would look something like this, and say for the sake of things we just have a function that simply returns the input as opposed to the async function:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: '', textfield: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
this.setState({textfield: event.target.value})
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
<p>{this.state.textfield}</p>
</form>
);
}
}
However we still run into the issue of updating the form itself.)
Edit
Is this a specific issue with forms elements? When someone is posting a comment on a blog is there a different component entirely that I should be using? I was drawn to forms because they take user input and then give you something that you can manipulate but is it maybe that they are immutable once you set them up?
At the time of writing this I am beginning to consider that this is when I should export a stored value, say textfield in my case, and just pass it into an entirely new component. Will update.
The problem is I seemingly have no way of updating the rendered form because it is either outside of postName's function scope or anything modified by postName even if instantiated outside of form cannot actually change the states.
When it comes to React, at the end of the day, if you want to make it re-render you need to update React state. What you're doing here with textfield is updating a JS variable, but React doesn't have a way of knowing that that variable is different, so it won't know to re-render.
Notice what you're doing with your input. Because you've set value, it's a "controlled input" and therefore the only reason you can see the text you type is because you're updating React state and triggering a re-render with every key press.
You can do the same thing with the result of your network request. Once the request finishes, you can update a piece of state, which will trigger the re-render. Note that if you don't use that state (e.g. by rendering out some of it) you won't be able to tell that React has re-rendered just from looking at it the browser.
Here's a minimally different example that makes a network request based on your form input and renders the result. (The variable names don't make a ton of sense for this API, just wanted to keep the diff small).
function Form() {
const [name, getNFT] = useState("");
const [jokes, setJokes] = useState([]);
let postName = (e) => {
let output;
async function getInfo(e) {
e.preventDefault();
try {
// swapped this out to a free public API so we could see a result
const resp = await axios.get(`https://icanhazdadjoke.com/search?term=${name}`, {
headers: {
// you need to tell this API you want the results as JSON, instead of HTML (the default)
'Accept': 'application/json'
}
});
console.log(resp.data);
output = resp.data;
return output;
} catch (error) {
console.error(error);
}
}
getInfo(e).then((output) => {
console.log("output is outside of function scope", output);
// this is the big change!
setJokes(output.results);
});
};
return (
<div className="App">
<form onSubmit={postName}>
<input
type="text"
value={name}
onChange={(e) => getNFT(e.target.value)}
/>
<button type="submit">Get Collection's Gallery</button>
<p>{jokes[0]?.joke}</p>
</form>
</div>
);
}
And here's a version that does the same thing but cleans it up a little bit, just to compare:
// this is a pure js function - no react! It just makes the request and returns the result
async function searchJokes(jokeTerm) {
try {
// swapped this out so we could see a result
const jokesResponse = await axios.get(
`https://icanhazdadjoke.com/search?term=${jokeTerm}`,
{
headers: {
// you need to tell this API you want the results as JSON, instead of HTML (the default)
Accept: "application/json"
}
}
);
console.log("jokes response", jokesResponse);
return jokesResponse.data;
} catch (error) {
console.error(error);
}
}
function Form() {
const [searchTerm, setSearchTerm] = useState("");
const [jokes, setJokes] = useState([]);
return (
<div className="App">
<form
onSubmit={async (e) => {
e.preventDefault();
// whereas the pure js function can just return its request normally, inside here we need
// to take the result and stick it in React state.
const jokesData = await searchJokes(searchTerm);
setJokes(jokesData.results);
}}
>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit">Search Jokes</button>
<p>{jokes[0]?.joke}</p>
</form>
</div>
);
}
(Sandbox). To be clear, the above is definitely not perfect!
getInfo(e).then(output =>{console.log("output is outside of function scope", output)
})
textfield = output
return textfield
Setting aside React, one thing to remember is that the code inside the function inside then will run when getInfo(e) is done fetching its information from the internet, but the lines after run right away without waiting, because the getInfo call returns a Promise. In JS, async logic like that is non-blocking! You can see above how you use await to avoid needing a then and the nesting. (But, it's worth remembering that async/await is just fancy syntax for Promises and .then, so anything you can do with one you can do with the other).
Also, because of function scoping, you won't be able to access output outside of the then function. You'd have to do that variable assignment inside that function, or use async await to flatten things a bit.
At one point I thought that this was most likely because this is a more appropriate instance of using a class, I however can't use useState and therefore would have no way of storing the content written out in the submit form. What am I missing here?
In React, it used to be that you needed classes for more complex components, like ones with state, and could only use functions for presentational components. But ever since they introduced hooks, class components and functional components can do the same things in different ways, so what you're doing here is fine! I would stick with function components like you've got while you're learning.
Outside of React, classes are a good option when dealing with state (though you can also do equivalent things with nested functions). But you'd still need some way to map updates to your vanilla JS class state to React state. To start, I would focus mostly on storing state within React, and as you build bigger apps you can either spend some time learning how to tie them into your own custom classes, or bring in a state management library (Redux is probably the most well known) to handle that for you.

useState and mapping

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>
)

Multiple submissions in one Textfield overrides the previous value that was saved

I have this form with multiple checkboxes and below it, I also have the others where the user can enter any value. The problem is that if I'll enter a value for the 2nd time, it will remove the previous value entered by the user.
Assuming that I've entered books for my first submit. Now, I want to submit another value for the others again, but this time it will be movies. I want to save in the firestore the both of these values; books and movies. The problem is that if I'll submit movies, this will override the previous one books, meaning it will replace books. How can I avoid that and at the same time display the multiple values entered by the user in the field others?
Below are the codes:
const sample = (props) => {
const [others, setOthers] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.set(
{
1: {
choices,
others
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
return (
<>
<form onSubmit={handleSubmit}>
<FormGroup>
//codes for the checkboxes here
<TextField
type="text"
label="Others:"
value={others}
onChange={(e) => setOthers(e.target.value)}
multiline
/>
</FormGroup>
<button type="submit">Submit</button>
<br />
</form>
</>
);
};
export default sample;
im going to preface this with im not familiar with react, but i do mess around with firestore alot. so the syntax maybe different for you.
but the first thing i notice is that you're using const ref = user.set to make the document. this is fine for first time creating a document, but if you use '.set' on an existing document it will override all the data in that document with whatever you're attempting to update it with.
you should use const ref = user.update to update fields in the document.
the 2nd bit is lets say you want to update the 'others' field. it would still override the data in that field even if you use '.update'. update is doing just that, its updating the field in question with whatever you're trying to update it with. what you want to do is add to it.
so your 'others' field needs to be an array and in order to add new values into it without overriding the previous data you need to use an arrayUnion.
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.update(
{
1: {
choices,
others: firebase.firestore.FieldValue.arrayUnion(others),
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
now i dont know how imports work in react but in VUEjs you'd need to import import firebase from "firebase/compat/app"; in the script tag in order to use the that firebase feature.
if you want to remove an item from that others array then use.
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.update(
{
1: {
choices,
others: firebase.firestore.FieldValue.arrayRemove(item), //item = whatever it is you're trying to remove.
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
From the React docs,
this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state. To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument.
prevState is a name to the argument passed to setState callback function. What it holds is the value of state before the setState was triggered by React.
So if multiple setState calls are updating the same state, batching setState calls may lead to incorrect state being set. Consider an example.
Your 'others' field needs to be an array and in order to add new values into it without overriding the previous data you need to use prevState.
If you don’t want to use setState you can use
prevState in useState React Hook with Javascript spread operator to
concatenate the previous values with current values in array
Something like this.

I want to access my state variable from one component to other

I have a react query which writes the state variable- follower, and I want to access this variable in other component to find its .length can someone tell me how do I do it
const ModalFollower = ({profile}) => {
const [follower,setFollower] = useState([])
const {
data: followerName,
isLoading: followerLoading,
isFetching: followerFetching
} = useQuery(["invitations", profile?.id], () => {
getFollowers(profile?.id).then((response) => {
if (response) {
setFollower(response);
}
});
});
return(
{
!followerLoading && (
follower.map((e) => {
return(<>
<p>{e.requested_profile.Userlink}</p>
</>}
)
}
)
I want to access the length of follower in some other component
There is no need to copy data from react-query to local state, because react-query is a full-blown state manager for server state. As long as you use the same query key, you will get data from its cache. This is best abstracted away in custom hooks.
Please be aware that with the default values, you will get a "background refetch" if a new component mount, so you will see two network requests if you use it twice. That might look confusing at first, but it is intended, as it is not react-query's primary goal to reduce network requests, but to keep your data on the screen as up-to-date as possible. So when a new component mounts that uses a query, you'll get the stale data from the cache immediately, and then a background refetch will be done. This procedure is called stale-while-revalidate.
The best way to customize this behaviour is to set the staleTime property to tell react-query how long your resource is "valid". For that time, you will only get data from the cache if available. I've written about this topic in my blog here: React Query as a State Manager.
React Query also provides selectors, so if your second component is only interested in the length, this is what my code would look like:
const useInvitations = (profile, select) =>
useQuery(
["invitations", profile?.id],
() => getFollowers(profile?.id),
{
enabled: !!profile?.id
select
}
)
Note that I also added the enabled property because apparently, profile can be undefined and you likely wouldn't want to start fetching without that id.
Now we can call this in our main component:
const ModalFollower = ({profile}) => {
const { data } = useInvitations(profile)
}
and data will contain the result once the promise resolves.
In another component where we only want the length, we can do:
const { data } = useInvitations(profile, invitations => invitations.length)
and data will be of type number and you will only be subscribed to length changes. This works similar to redux selectors.

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