How to add event to react function and re-render function - javascript

I have a function that renders content to page based on a state populated by API data, but I need to have an onClick event to refine that content;
So currently getPosts returns information from the state 'posts' which is provided with data from our API, but i want to filter this content further, so my idea is to have some sort of event listener, and if actioned, change the data coming out of getPosts.
constructor() {
super();
this.state = {
posts: ""
}
this.getPosts = this.getPosts.bind(this);
}
async componentWillMount(){
var data = await api.posts();
this.setState({posts: data.data});
console.log(this.state.posts);
}
getPosts(type){
if(this.state.posts.length){
return this.state.posts.map((content,index) => {
var url = content.Title.replace(/[^\w\s]/gi, '');
url = url.replace(/\s+/g, '-').toLowerCase();
if(type === content.PostType){
//output something different
}
else{
return(
<Col md={4} className="mb-4" key={index}>
{content.title}
</Col>
);
}
})
}
}
render() {
return (
<div>
<p><button onClick={()=>{this.getPosts('blog')}}>blog</button> <button onClick={()=>{this.getPosts('news')}}>news</button></p>
{this.getPosts()}
</div>
)
}
So my getPosts works fine without any type, how to do tell it to re-output the function on the page based in the onClick event?

Without getting into the complexities of context and keys, a component requires a change in props or state to re-render. To read more about state and component life-cycle, the docs have a great explanation for class components.
Your component does not re-render after the onClick event handler's call to getPosts because getPosts does not update internal component state. getPosts works within render because those values are being returned to React. By using getPosts as an onClick event handler, you are creating React elements and trying to return them to the window.
What follows should be treated as psuedo code that shows how to trigger your component to render different posts:
Consider adding another key to state in your constructor,
constructor(props) {
super(props);
this.state = {
posts: "",
type: null
};
this.getPosts = this.getPosts.bind(this);
this.onClick = this.onClick.bind(this);
}
and creating a click handler that doesn't try to return React elements
function onClick(evt) {
this.setState({ type: evt.target.value });
}
and values to your buttons
<button onClick={this.onClick} type="button" value="blog">blog</button>
Now your button will update state with your new post type, causing your component to re-render:
render() {
return (
<div>
<p>
<button onClick={this.onClick} type="button" value="blog">blog</button>
<button onClick={this.onClick} type="button" value="news">news</button>
</p>
{this.getPosts()}
</div>
);
}
With the content type being stored in state, you can now implement your getPosts call in any way that works for you. Good luck!
It strays from the question asked, but it is worth noting componentWillMount is being deprecated, and componentDidMount is a preferable life-cycle function for side-effects and asynchronous behavior. Thankfully, the documentation has lots of details!

Ok so you should start by changing your default this.state to
this.state = {
posts: []
}
remember that you want to iterate over an array of data instead of iterate a string, that will throw an error if you do that, so better keep from the beginning the data type you want to use.
Then you need to separate the responsibility for your getPosts method, maybe getPostByType is a better name for that, so you have
getPostByType(type) {
// if type is same as content.PostType then return it;
const nextPosts = this.state.posts.filter((content) => type === content.PostType);
this.setState({ posts: nextPosts });
}
and finally you can iterate over posts, like this
render() {
// better use content.id if exists instead of index, this is for avoid problems
// with rerender when same index key is applied.
const posts = this.state.posts.map((content, index) => (
<Col md={4} className="mb-4" key={content.id || index}>
{content.title}
</Col>
));
return (
<div>
<button onClick={() => this.getPostByType('blog')}>Show Blog Posts</button>
{posts}
</div>
);
}
Then you can use getPostsByType any time in any click event passing the type that you want to render.

Related

Appending JSX to a state variable in ReactJS

I have a state variable which stores JSX code that will be rendered to the DOM in the render() method.
Here is the structure of state and the state variable jsxComp that I currently have,
state = {
isLoggedin: false,
jsxComp: null
}
What I am trying to do is I am trying to append JSX code ( a div ) into this variable inside a for loop as follows,
for(let i=0; i<postCount; i++){
this.setState({
//Append a div to the jsxComp variable
})
}
How can I do that? + operator does not seem to work in this case.
Never store actual elements in state. State should only contain the pure data to render, not the actual markup. Instead make your render() method conditionally render based on the state:
class MyComp extends React.Component {
state = {
isLoggedIn: false,
};
render() {
const {isLoggedIn} = this.state;
return (
<div>
{/* conditionally render based on isLoggedIn */}
<p>{isLoggedIn ? 'You are logged in' : 'Please log in.'}</p>
<button onClick={() => this.setState({isLoggedIn: true})}>
Log In
</button>
</div>
);
}
}
Your render() method will be called whenever the state changes. React will then diff the result of the render and update the DOM where elements changed.
You also should not call setState() in a loop. First collect the changes and then call setState with the whole changed array. To actually append something to an existing array in state you would do:
this.setState(state => {jsxComp : [...state.jsxComp, newElement]});

Managing multiple calls to the same Apollo mutation

So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))

React re-render conditional on state change without changing the conditional

I've got a conditional that displays an editor while a certain prop remains true. The thing is, the data with which that editor is rendered with should change every time I select another object with which to populate that editor.
However, because the prop responsible for the conditional rendering doesn't change, even though the data with which the editor is rendered does, it refuses to re-render on state change.
I'm not particularly good at React, so, hopefully someone can explain how I can get around this little hiccup.
Conditional render
{this.state.showEditor ? (<BlockEditor routine={this.state.editorObject} />) : null}
Method that is being called.
handleShowEditor = routine => {
this.setState({ showEditor: true });
this.setState({ editorObject: routine });
};
The editor component
export default class BlockEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
routine: this.props.routine
};
}
render() {
return (
<div>
<Editor
autofocus
holderId="editorjs-container"
onChange={data => this.handleSave(data)}
customTools={{}}
onReady={() => console.log("Start!")}
data={this.props.routine.description}
instanceRef={instance => (this.editorInstance = instance)}
/>
</div>
);
}
}
Is there a reason for setting state separately? Why not
handleShowEditor = routine => {
this.setState({
showEditor: true,
editorObject: routine
});
};
Keep in mind that setState is asynchronous and your implementation could lead to such weird behaviour.
If you are still looking for an answer i have faced the same problem working with the same [Editor.JS][1] :).
This worked for me with functional component:
// on change fires when component re-intialize
onChange={async (e) => {
const newData = await e.saver.save();
setEditorData((prevData) => {
console.log(prevData.blocks);
console.log(newData.blocks);
if (
JSON.stringify(prevData.blocks) === JSON.stringify(newData.blocks)
) {
console.log("no data changed");
return prevData;
} else {
console.log("data changed");
return newData;
}
});
}}
// setting true to re-render when currentPage data change
enableReInitialize={true}
Here we are just checking if data changes assign it to editorData component state and perform re-render else assign prevData as it is which will not cause re-render.
Hope it helps.
Edit:
i am comparing editor data blocks change which is array.
of course you need to perform comparison of blocks more deeply than what i am doing, you can use lodash for example.
[1]: https://github.com/editor-js/awesome-editorjs
As setState is asynchronous you can make another call in its callback.
Try like this
handleShowEditor = routine => {
this.setState({
showEditor: true
}, () =>{
this.setState({
editorObject: routine
)}
});
};

React - change this.state onClick rendered with array.map()

I'm new to React and JavaScript.
I have a Menu component which renders an animation onClick and then redirects the app to another route, /coffee.
I would like to pass the value which was clicked (selected) to function this.gotoCoffee and update this.state.select, but I don't know how, since I am mapping all items in this.state.coffees in the same onClick event.
How do I do this and update this.state.select to the clicked value?
My code:
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
coffees:[],
select: '',
isLoading: false,
redirect: false
};
};
gotoCoffee = () => {
this.setState({isLoading:true})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={`/coffee/${this.state.select}`} />)
}
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map(c =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee()}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
I have tried passing the value like so:
gotoCoffee = (e) => {
this.setState({isLoading:true,select:e})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.select)
}
an like so:
<div onClick={(c) => this.gotoCoffee(c)}
or so:
<div onClick={(event => this.gotoCoffee(event.target.value}
but console.log(this.state.select) shows me 'undefined' for both tries.
It appears that I'm passing the Class with 'c'.
browser shows me precisely that on the uri at redirect:
http://localhost/coffee/[object%20Object]
Now if I pass mapped 'c' to {this.renderCoffee(c)}, which not an onClick event, I manage to pass the array items.
But I need to pass not the object, but the clicked value 'c' to this.gotoCoffee(c), and THEN update this.state.select.
How do I fix this?
You can pass index of element to gotoCoffee with closure in render. Then in gotoCoffee, just access that element as this.state.coffees[index].
gotoCoffee = (index) => {
this.setState({isLoading:true, select: this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map((c, index) =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
so based off your code you could do it a couple of ways.
onClick=(event) => this.gotoCoffee(event.target.value)
This looks like the approach you want.
onClick=() => this.gotoCoffee(c)
c would be related to your item in the array.
All the answers look alright and working for you and it's obvious you made a mistake by not passing the correct value in click handler. But since you're new in this era I thought it's better to change your implementation this way:
It's not necessary use constructor at all and you can declare a state property with initial values:
class Menus extends Component{
state= {
/* state properties */
};
}
When you declare functions in render method it always creates a new one each rendering which has some cost and is not optimized. It's better if you use currying:
handleClick = selected => () => { /* handle click */ }
render () {
// ...
coffees.map( coffee =>
// ...
<div onClick={ this.handleClick(coffee) }>
// ...
}
You can redirect with history.replace since you wrapped your component with withRouterand that's helpful here cause you redirecting on click and get rid of renderCoffee method:
handleClick = selected => () =>
this.setState(
{ isLoading: true},
() => setTimeout(
() => {
const { history } = this.props;
this.setState({ isLoading: false });
history.replace(`/${coffee}`);
}
, 5000)
);
Since Redirect replaces route and I think you want normal page change not replacing I suggest using history.push instead.
You've actually almost got it in your question. I'm betting the reason your state is undefined is due to the short lived nature of event. setState is an asynchronous action and does not always occur immediately. By passing the event off directly and allowing the function to proceed as normal, the event is released before state can be set. My advice would be to update your gotoCoffee function to this:
gotoCoffee = (e) => {
const selectedCoffee = e.target.value
this.setState({isLoading:true,select:selectedCoffee},() =>
{console.log(this.state.select})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
Note that I moved your console.log line to a callback function within setState so that it's not triggered until AFTER state has updated. Any time you are using a class component and need to do something immediately after updating state, use the callback function.

Cannot retrieve single item from an array stored in the state of React

Before I get bashed: "I am new to React!"
The url to the .json file is:
JSON FOR GETTING QUOTES
I am maintaining a state in my app which holds an array of quotes and the key value inside it.
constructor(props){
super(props);
this.state = {
quotes: [],
key: 0
};
this.handleClick= this.handleClick.bind(this)
}
The state is updated on getting a request to the url using axios.
componentDidMount() {
const url = 'https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json';
axios.get(url).then((res) =>{
const items = (res.data.quotes)
this.setState({quotes: items})
console.log(this.state)
}).catch(err => console.log(err))
}
Then I wish to pass get only one quote each time a click is made on the button.
For which I render a "DivElement" passing a specific quote as a prop as following:
render() {
return (
<div>
<button onClick={this.handleClick}>
Click Me
</button>
<DivElement
currentquote = {this.state.quotes[this.state.key].quotes}
/>
</div>
);
}
"DivElement" declaration:
function DivElement(props){
console.log(props.currentquote)
return <p>{props.currentquote}</p>}
This is the TypeError I get:
TypeError: this.state.quotes[this.state.key] is undefined
Things I have tried:
Didn't work Use JSON.parse method to set the state.
What I wish to do: To display a random quote based on random key generated by onClick method.
No render is asynchronous and it doesn't wait for API callback.
you have to conditionally render your component like this:
render() {
return (
this.state.quotes.length && ( <div>
<button onClick={this.handleClick}>
Click Me
</button>
<DivElement
currentquote={this.state.quotes[this.state.key].quotes}
/>
</div> )
);
}
}
Please let me know if the issue still persists,

Categories

Resources