Invalid Hook Call Full Stack Open 2021 Phonebook - javascript

I am new to React.
I am trying to use Filter component but it is giving me this error: "Invalid hook call. Hooks can only be called inside of the body of a function component."
I think the problem is somewhere in the onChange={handleFilterChange} but I do not know what. Any clues?
Here is my App.js:
import React, { useState } from 'react'
import Filter from './components/Filter'
import Person from './components/Person'
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', number: '040-123456' },
{ name: 'Ada Lovelace', number: '39-44-5323523' },
{ name: 'Dan Abramov', number: '12-43-234345' },
{ name: 'Mary Poppendieck', number: '39-23-6423122' }
])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const [filter, setFilter] = useState('')
const handleNameChange = (event) => {
console.log("name", event.target.value)
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
console.log("number", event.target.value)
setNewNumber(event.target.value)
}
const handleFilterChange = (event) => {
console.log("filter", event.target.value)
setFilter(event.target.value)
}
const peopleToShow = filter
? persons.filter(person => person.name.toLowerCase().includes(filter.toLowerCase()))
: persons
const handleAddPerson = (event) => {
// prevent default form action
event.preventDefault();
console.log('button clicked', event.target);
console.log("newname", newName)
let checkNameExists = persons.filter(person => (person.name === newName))
let checkNumberExists = persons.filter(person => (person.number === newNumber))
if (checkNameExists.length === 0 && checkNumberExists.length === 0) {
const nameObject = {
name: newName,
number: newNumber
}
// dont mutate state. use set method (make new object)
setPersons(persons.concat(nameObject))
} else if (checkNameExists.length !== 0) {
alert(`${newName} is already added to phonebook`);
} else {
alert(`${newNumber} is already added to phonebook`);
}
// reset name and number
setNewName('')
setNewNumber('')
}
return (
<div>
<h2>Phonebook</h2>
<Filter value={filter} onChange={handleFilterChange}/>
{/* <div>
filter shown with:<input value={filter}
onChange={handleFilterChange} />
</div> */}
<h2>add a new contact</h2>
<form onSubmit={handleAddPerson}>
<div>
name:<input value={newName}
onChange={handleNameChange} />
</div>
<div>number: <input value={newNumber}
onChange={handleNumberChange} /></div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
<div>
{peopleToShow.map(person =>
<Person key={person.name} person={person} />
)}
</div>
</div>
)
}
export default App
Here is my Filter.js:
import React from 'react';
const Filter = (props) => {
return (
<div>
filter shown with:<input value={props.filter}
onChange={props.handleFilterChange} />
</div>
)
}
export default Filter

you can not pass the reference of your setFilter func to anywhere else in the props. That's why you've used the handler function in the first place.. you should rename the prop. its a better practice.
and then again, in your filter js you should use the prop setFilter that you've passed. you haven't passed handleFilterChange func in the props.. you tried to import that func.. but I believe you have not exported it.. and you did not follow the named import convention..

Change
<Filter value={filter} onChange={handleFilterChange}/>
to
<Filter filter={filter} handleFilterChange={handleFilterChange} />

Related

Conditional Component Rendering using UseState

I have a form in a page, when the user inputs the name of a new student and clicks submit, I want the content of that component (the form) to be completely replaced by the submitted name. How can I achieve this (Replace the form with the list onsubmit)?
I have read that I can use conditional rendering to toggle components, but it's not really clear to me how i can apply it here.
StudentListResult.Jsx
import React, { useState } from "react";
import StudentForm from "./StudentForm";
import StudentList from "./StudentList";
const StudentListResult = () => {
const [newStudent, setNewStudent] = useState("");
const [students, setStudentsList] = useState([]);
return (
<div>
<div>
<StudentForm
newStudent={newStudent}
setNewStudent={setNewStudent}
students={students}
setStudentsList={setStudentsList}
/>
</div>
<div>
<StudentList students={students} setStudentsList={setStudentsList} />
</div>
</div>
);
};
export default StudentListResult;
StudentListForm
import React from "react";
import { v4 as uuidv4 } from "uuid";
const StudentListForm = ({
newStudent,
setNewStudent,
students,
setStudentsList,
}) => {
const addStudent = (event) => {
event.preventDefault();
setStudentsList([...students, { id: uuidv4(), name: newStudent }]);
setNewStudent("");
};
return (
<form onSubmit={addStudent}>
<div>
<input
value={newStudent}
type="text"
placeholder="Student Name"
onChange={(e) => setNewStudent(e.target.value)}
/>
</div>
<div>
<button>Submit</button>
</div>
</form>
);
};
export default StudentListForm;
StudentList.jsx
import React from "react";
const StudentList = ({ students = [], setStudentsList }) => {
return (
<div>
{students.map((student) => (
<ul key={student.id}>
<li>
<p>{student.name}</p>
</li>
</ul>
))}
</div>
);
};
export default StudentList;
So you want to show the form if not submitted and show the list if submitted? You can add a piece of state called submitted and do simple conditional rendering.
const StudentListResult = () => {
const [submitted, setSubmitted] = useState(false)
return (
{submitted ? <StudentList /> : <StudentListForm />}
);
};
And then in your addStudent function, set submitted.
const addStudent = (event) => {
// ...
setSubmitted(true)
}
If you want change form and list visibility state, you need pass custom function to form component:
StudentListResult.jsx:
const StudentListResult = () => {
const [newStudent, setNewStudent] = useState("");
const [students, setStudentsList] = useState([]);
const [getFormSubmitted, setFormSubmitted] = useState(false);
const setCompletedForm = () => {
setFormSubmitted(!getFormSubmitted);
};
return (
<div>
{getFormSubmitted ? (
<div>
<StudentList students={students} setStudentsList={setStudentsList} />
</div>
) : (
<div>
<StudentForm
newStudent={newStudent}
setNewStudent={setNewStudent}
students={students}
setStudentsList={setStudentsList}
onComplete={setCompletedForm}
/>
</div>
)}
</div>
);
};
Then call this function if form is submitted and all conditions is true
StudentListForm.tsx:
const StudentListForm = ({
newStudent,
setNewStudent,
students,
setStudentsList,
onComplete
}) => {
const addStudent = (event) => {
event.preventDefault();
setStudentsList([...students, { id: uuidv4(), name: newStudent }]);
setNewStudent("");
onComplete();
};

React function prop gives "TypeError is not a function"

I am new to react and I'm trying to pass a function as a prop to a child component.
In my case this is the parent component:
export default function Game() {
const [gameStarted, setGameStarted] = useState(false)
const [gameSettings, setGameSettings] = useState({})
useEffect(() => {
//setGameStarted(true);
}, [gameSettings]
)
return (
<>
{!gameStarted &&
<div className="game-form">
<GameSelection handleGameSelection={(settings)=> setGameSettings(settings)}/>
</div>}
</>
)}
My child component is:
export default function GameSelection({handleGameSelection}) {
const [labels, setLabels] = useState([])
const [gameMode, setGameMode] = useState('')
const [selectedLabels, setSelectedLabels] = useState([])
const [formError, setFormError] = useState(null)
// create label values for react-select
useEffect(() => {
if(document) {
setLabels(document.cards.map(card => {
return { value: {...card}, label: card.label}
}))
}
}, [document])
const handleSubmit = (e) => {
e.preventDefault()
try{
const gameSettings = {
mode: gameMode.value,
selected: selectedLabels.map((card) => ({...card.value})),
}
handleGameSelection(gameSettings)
}
catch(error){
console.log(error)
}
}
return (
<>
<h2 className="page-title">Please select your game</h2>
<form onSubmit={handleSubmit}>
<label>
<span>Mode:</span>
<Select
onChange={(option) => setGameMode(option)}
options={gameModes}
/>
</label>
<label>
<span>Select labels:</span>
<Select
onChange={(option) => setSelectedLabels(option)}
options={labels}
isMulti
/>
</label>
<button className="btn" >Start game</button>
{formError && <p className="error">{formError}</p>}
</form>
</>
)}
My form works but when I submit the form I keep getting the error TypeError: handleGameSelection is not a function. I tried everything. I have created a separate function in the parent component and gave that as a prop to the child. That also didn't work. I don't know what I am doing wrong. Any ideas?
Run this function inside useEffect, because currently, you are running this function before component is fully mounted and this function is propably undefined... or you can try to use if(typeof handleGameSelection === 'function') to check if its already initialized

react checkbox onChange to call function is check state is true in typescript

I am exploring the react onChange feature. The functionality I would like to do is
when checkbox is selected, I would like to update local data to add some value
when checkbox is unselected, I would like to just populate the original data
the code I have seems do the reverse of what I want. any can provide some guideline?
codesandbox
import "./styles.css";
import { useState } from "react";
export default function App() {
const localData = [{ name: "apple", phone: 12345 }];
const [check, setCheck] = useState(false);
const [data, setData] = useState(localData);
const handleOnChange = () => {
setCheck(!check);
check ? setData([...data, { name: "amazon", phone: 222 }]) : setData(data);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<p>
<input type="checkbox" check onChange={handleOnChange}></input>
</p>
<p>{JSON.stringify(data)}</p>
</div>
);
}
The setCheck runs asyncranously. So in the function handleOnChange, right after setCheck(!check), check's value has not been set to the new value yet.
You could use an effect here since you want the side-effect of changing data from changing check. This way, whenever check is done changing, the effect triggers:
const handleOnChange = () => {
setCheck(!check);
};
useEffect(() => {
check &&
setData((previous) => [...previous, { name: "amazon", phone: 222 }]);
}, [check]);
Updated sandbox: https://codesandbox.io/s/quirky-rain-hmhkk
There is no need of using two states, that solution will give you more problems when you want to add more options.
I would go for an approach similar to this one:
import "./styles.css";
import { useState } from "react";
const localData = [{ name: "apple", phone: 12345 }];
const extraData = [{ name: "amazon", phone: 222 }];
export default function App() {
const [data, setData] = useState(localData);
const handleOnChange = (item) => {
if (!data.find(({ name }) => name === item.name)) setData([...data, item]);
else setData(data.filter(({ name }) => name !== item.name));
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<p>
{extraData.map((item) => {
return (
<input
key={item.name}
type="checkbox"
checked={data.includes(({ name }) => name === item.name)}
onChange={() => handleOnChange(item)}
/>
);
})}
</p>
<p>{JSON.stringify(data)}</p>
</div>
);
}
This will allow you to add more checkboxes in the future.
So, you want the data to be added to data when the checkbox IS checked? Just invert the condition of the ternary that you use in handleOnChange:
import "./styles.css";
import { useState } from "react";
export default function App() {
const localData = [{ name: "apple", phone: 12345 }];
const [check, setCheck] = useState(false);
const [data, setData] = useState(localData);
const handleOnChange = () => {
setCheck(!check);
!check ? setData([...data, { name: "amazon", phone: 222 }]) : setData(data);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<p>
<input type="checkbox" check onChange={handleOnChange}></input>
</p>
<p>{JSON.stringify(data)}</p>
</div>
);
}

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

Update a react state (array of objects) from a child component

I would like to update the parent state from child component, which renders each object of the array of objects. The main goal of the child component is to update the original value from the array of objects.
I've the following code
Parent:
import { useState } from 'react';
import ExpenseItem from './expenseItem';
function Update({ data }) {
const [ expenses, setExpenses ] = useState(data);
return (
<div>
{expenses.map((expense, index) => {
return <ExpenseItem key={index} {...expense} />;
})}
<button>Save</button>
</div>
);
}
export default Update;
child:
import { useState, useRef } from 'react';
function ExpenseItem({ description, date, credit, debit }) {
const [ edit, setEdit ] = useState(false);
const [ expenseDescription, setExpenseDescription ] = useState(description);
const textInput = useRef();
const renderDefaultView = () => {
return <h3 onDoubleClick={() => setEdit(true)}>{expenseDescription}</h3>;
};
const renderEditView = () => {
return (
<div>
<input
type="text"
ref={textInput}
defaultValue={expenseDescription}
onDoubleClick={() => setEdit(true)}
/>
<button onClick={() => setEdit(false)}>X</button>
<button onClick={() => updateValue()}>OK</button>
</div>
);
};
const updateValue = () => {
const value = textInput.current.value;
setExpenseDescription(value);
textInput.current.defaultValue = value;
setEdit(false);
};
return (
<div>
{edit ? renderEditView() : renderDefaultView()}
<span>{date}</span>
<p>{debit}</p>
<p>{credit}</p>
</div>
);
}
export default ExpenseItem;
Once way, is to pass the parent state property (expenses) and the function that updates it (setExpenses) to the child Component via the props:
Parent:
import React from 'react';
import ReactDOM from 'react-dom';
import { useState } from 'react';
import ExpenseItem from './ExpenseItem';
function Update({ data }) {
const [ expenses, setExpenses ] = useState(data);
return (
<div>
Checking: { expenses[0].description } | { expenses[1].description }
<hr/>
{expenses.map((expense, index) => {
return <ExpenseItem key={index} index={index} expenses={expenses} setExpenses={setExpenses} />;
})}
<button>Save</button>
</div>
);
}
export default Update;
Child:
import React from 'react';
import { useState, useRef } from 'react';
function ExpenseItem( props ) {
let { description, date, credit, debit } = props.expenses[props.index];
const setExpenses = props.setExpenses;
const [ edit, setEdit ] = useState(false);
const [ expenseDescription, setExpenseDescription ] = useState(description);
const textInput = useRef();
const renderDefaultView = () => {
return <h3 onDoubleClick={() => setEdit(true)}>{expenseDescription}</h3>;
};
const renderEditView = () => {
return (
<div>
<input
type="text"
ref={textInput}
defaultValue={expenseDescription}
onDoubleClick={() => setEdit(true)}
/>
<button onClick={() => setEdit(false)}>X</button>
<button onClick={() => updateValue()}>OK</button>
</div>
);
};
const updateValue = () => {
const value = textInput.current.value;
setExpenseDescription(value);
textInput.current.defaultValue = value;
setEdit(false);
const expenses = [ ...props.expenses ]; // Get a copy of the expenses array
// Replace the current expense item
expenses.splice( props.index, 1, {
description: value, date, credit, debit
});
// Update the parent state
setExpenses( expenses );
};
return (
<div>
{edit ? renderEditView() : renderDefaultView()}
<span>{date}</span>
<p>{debit}</p>
<p>{credit}</p>
</div>
);
}
export default ExpenseItem;
Working demo
This can get really complicated as you move along, so the best option is to look for some sort of State Management solution, like using the Context API.
Also, take a look at this interesting post that talks about using the map index value as a key value: Index as a key is an anti-pattern

Categories