I made a todo app and I wanted to improvise it by showing all completed task if "Show completed task" is checked
Checking "Show completed task" works fine, but unchecking it doesn't execute the "else statement".
Originally what wanted is if its unchecked it should give me all the state, completed or not. The code from 'else statement' is just for debugging. It should at least give me the array[0] and array[2], tried console logging it, and it throws an undefined.
PS: tried to console log something upon unchecking, unchecked is working.
filterCompleted = (e) => {
if(e.target.checked){
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === true})]})
}
else{
//code below just for debugging but this does not execute
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === false})]})
}
}
This will only ever work first time, beyond that the state will only ever hold all completed tasks or all uncompleted tasks.
You should introduce a new state e.g.
constructor(prop) {
this.state = {
todos: [],
filterCompleted: false
}
}
...
filterCompleted = e => this.setState({ filterCompleted: e.target.checked })
Then when rendering the list you can simply just exclude items that aren't completed e.g.
const items = this.state
.todos
.filter(x => !this.state.filterCompleted || x.completed)
.map(x => <TodoItem {...x} />)
The Problem lies in the following line
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === true})]})
You are just filtering the items without channging them. You have to create a new array and set the value of the item to true/false. Something like this:
let newTodos = this.state.todos
newTodos[index].checked = true
this.setState({todos: newTodos})
index being some string/int that identifies the checkbox itself.
I think you have to leave the props and set a new state variable filterCompleted and during render filter all todoitems:
// when checked:
this.setState({ filterCompleted: true/false })
// during render (with hooks better with useMemo...):
todosToReder = this.state.filterCompleted ? this.state.todos.filter(el => el.completed) : this.state.todos
Related
I’m running into an error that I could use some help on
Basically, I have a react app that is executing an HTTP call, receiving an array of data, and saving that into a state variable called ‘tasks’. Each object in that array has a key called ‘completed’. I also have a checkbox on the page called ‘Show All’ that toggles another state variable called showAll. The idea is by default all tasks should be shown however if a user toggles this checkbox, only the incomplete tasks (completed==false) should be shown. I can get all tasks to display but can’t get the conditional render to work based on the checkbox click
Here’s how I’m implementing this. I have the HTTP call executed on the page load using a useEffect hook and available to be called as a function from other change handlers (edits etc.)
Before I call the main return function in a functional component, I’m executing a conditional to check the status of ’ShowAll’ and filter the array if it's false. This is resulting in too many re-render errors. Any suggestions on how to fix it?
See simplified Code Below
const MainPage = () => {
const [tasks, setTasks] = useState([]); //tasks
const [showAll, setShowAll] = useState(true); //this is state for the checkbox (show all or just incomplete)
useEffect( ()=> {
axios.get('api/tasks/')
.then( response => { //this is the chained API call
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
}, []);
const fetchItems = (cat_id) => {
axios.get('/api/tasks/')
.then( response => {
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
};
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(!showAll)
console.log('Checkbox: ', showAll)
};
//this part updates the tasks to be filtered down to just the incomplete ones based on the checkbox value
if (showAll === false) {
setTasks(tasks.filter(v => v['completed']===false)); //only show incomplete tasks
}
return (
<div>
<label className="checkb">
<input
name="show_all"
id="show_all"
type="checkbox"
checked={showAll}
onChange={handleCheckboxChange}
/> Show all
</label>
<br/>
{ tasks && tasks.map((task, index) => {
return (
<div key={index} className="task-wrapper flex-wrapper">
<div >
{ task.completed === false ? (
<span> {index +1}. {task.task_description} </span> ) :
(<strike> {index +1}. {task.task_description} </strike>) }
</div>
<div>
<button
onClick={()=> modalClick(task)}
className="btn btn-sm btn-outline-warning">Edit</button>
<span> </span>
</div>
</div>
)
})}
</div>
);
};
export default MainPage;
Thanks
Two things to fix:
Use the checked property on event.target to update the state:
const handleCheckboxChange = ({target: { checked }}) => {
setShowAll(checked)
};
Filter as you want but don't update the state right before returning the JSX as that would trigger a rerender and start an infinite loop:
let filteredTasks = tasks;
if (!showAll) {
filteredTasks = tasks?.filter(v => !v.completed));
}
and in the JSX:
{ tasks && tasks.map should be {filteredTasks?.map(...
use e.target.value and useEffect :
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
if (!e.target.checked) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
};
or
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
};
useEffect(()=>{
if (showAll === false) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
},[showAll])
componentWillReceiveProps = (newProps) => {
console.log("data ");
let apiData = newProps.apiData;
if (apiData.topProfiles && apiData.topProfiles.success) {
let therapists = apiData.topProfiles.therapists
let hasMore = true
if (!therapists.length) {
hasMore = false
}
this.setState(() => ({
therapists: this.state.therapists.concat(therapists),
hasMore: hasMore,
pageLoading: false
}))
} else if (apiData.therapistsByName && apiData.therapistsByName.success) {
let therapists = apiData.therapistsByName.therapists,
resTitle = therapists.length ?
`Results for "${this.state.searchName}"`
: `Looks like there are no results for "${this.state.searchName}"`
this.setState(() => ({
therapists: therapists,
hasMore: false,
pageLoading: false,
resultsTitle: resTitle
}))
}
I read about componentWillReceiveProps and is not safe anymore. How can I implement it much more efficient.
I have a function which render a list of therapists, but if I am in therapist pages where the content is rendered, and click on "Specialists" (button path) again in Header, the therapist list duplicates.
renderTherapists = () => {
console.log("this state therapists: ", this.state.therapists)
let items = this.state.therapists.map( (t, idx) => (
<TherapistCard therapist={t} key={idx} />
))
return (
<div ref={0} className="therapist-list">
{ items }
</div>
)
}
console log after header button press
The code you have keeps concatenating therapists to the therapists array on the state because componentWillReceiveProps will run regardless if the props changed or not, as per React documentation.
The real question is, should you store something in your state if it's already available as a prop. If you have a list of therapists as a prop, why not simply render it instead of duplicating it and having to keep those two in sync?
If you really have to keep it in the state for whatever reason, you can use componentDidUpdate in which you can compare equality of previous and current props and set the state accordingly.
You need to compare the coming data with this.state.therapists state in componentWillReceiveProps before updating therapists state because every time componentWillReceiveProps is running and you concatenate even if the same data coming.
I'm working on a React project, and I have a section with "Saved Games".
The "Saved Games" section maps the "Saved Games" state.
This is what it looks like:
let SavedGamesList = <h1>Loading...</h1>;
if (this.props.savedGamesState.length < 1) {
SavedGamesList = <StyledNotSavedGames>Such empty</StyledNotSavedGames>;
}
if (this.props.savedGamesState.length >= 1) {
SavedGamesList = this.props.savedGamesState.map(game => (
<GameCard
key={game.game}
title={game.title}
hoursViewed={game.hours_viewed}
saved={true}
/>
));
}
When I try to delete a game, it deletes a random one not the one I clicked, or multiple games at once.
This is what the "GameCard" (Where the delete button is) looks like:
deleteGame = () => {
let gameName = this.props.title;
this.props.deleteGame(gameName); //This is the Redux dispatch
console.log(this.props.savedGamesState);
};
And this is how I try to change the state in the Reducer:
case actionTypes.DELETE_GAME:
let updatedGames = [
...state.savedGames.splice(
state.savedGames.findIndex(e => e.title === action.payload),
1
)
];
return {
...state,
savedGames: updatedGames
};
Edit: Dispatch to props:
deleteGame: (res) => dispatch({type: actionType.DELETE_GAME, payload: res})
I also noticed that the last game in the list can't be deleted, the state updated but the component doesn't re-render so it's not disappearing.
Something in the reducer probably is wrong, what do you think?
I think your problem is that the return value of splice is the array of removed games,
try something like that (note you can also use the filter method):
case actionTypes.DELETE_GAME:{
let updatedGames = [
...state.savedGames
];
updatedGames.splice(
updatedGames.findIndex(e => e.title === action.payload),
1
)
return {
...state,
savedGames: updatedGames
};
}
also I think it is better for you to use the key of the game to remove it and not the title unless the title is unique
I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?
i have an issue trying to create a "typehead" funcitonality in my app, i have an "input" that listen to onChange, and that onChange is calling to a Redux reducer that search for a tag in hole store, i need to retrive all matches with my search, over here everything is ok, but when i delete my search, my hole store is equals to my filtered results, and i want that when my search is empty it returns hole my store. (gif and code)
case 'SEARCH_BY_TAG':
let tag = action.tag
let filtered = state.slice(0)
if(tag != ""){
const copied = state.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(tag) > -1){
return true
}
})
})
return filtered = copied.filter(Boolean)
}else{
return filtered
}
break;
Instead of filtering things out inside your reducer, do it directly on render(), there is nothing wrong with that.
You can still use the SEARCH_BY_TAG action to keep track of the search keyword and use it to apply the filter when rendering your list.
I think you should refactor your state to change it from this:
[ item1, item2, item3, ... ]
to this:
{
query: '',
options: [ item1, item2, item3, ... ]
}
This way you can do what #Raspo said in his answer - do the filtering of the options in your render function.
Currently when you change the text in the search field, you are dispatching this action:
{
type: 'SEARCH_BY_TAG',
tag: 'new query'
}
I think you should change the action name, and the reducer code, to look more like this:
// note this is almost exactly the same as the old action
{
type: 'CHANGE_AUTOCOMPLETE_QUERY',
query: 'new query'
}
and the reducer could then change to this:
case CHANGE_AUTOCOMPLETE_QUERY:
return Object.assign({}, state, {
query: action.query
})
Note that in the reducer case I just wrote, the options part of the state isn't changed at all. It remains constant.
Now let's assume your current render function looks something like this:
const options = reduxState // not sure exactly how you get the state but whatever
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)
This code relies on getting the state as an array. You could change it in the new setup to do this:
const query = reduxState.query
const options = reduxState.options.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(query) > -1){
return true
}
})
})
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)