Function component doesn't re-render after updating array state in react - reactjs

I want to make a little react application that saves short text entries. The app shows all published entries and the user can add a new entry by writing and publishing it.
The applcation has an string-array (string[]) type. Every item in the array is an entry that has to be displayed in the frontend entries list.
I know that I can't push to the array because that doesn't changes the state directly (and react doesn't notice that it have to be re-rendered). So i am using this way to get the new state: oldState.concat(newEntry). But React doesn't re-render it.
Here is my whole react code:
function App() {
const [entries, setEntries] = useState([] as string[])
const publish = (entry: string) => {
setEntries(entries.concat(entry))
}
return (
<div>
<Entries entries={entries} />
<EntryInput publish={publish} />
</div>
)
}
function Entries(props: { entries: string[] }) {
return (
<div className="entries">
{props.entries.map((v, i) => { <EntryDisplay msg={v} key={i} /> })}
</div>
)
}
function EntryInput(props: { publish: (msg: string) => void }) {
return (
<div className="entry-input">
<textarea placeholder="Write new entry..." id="input-new-entry" />
<button onClick={(e) => { props.publish((document.getElementById("input-new-entry") as HTMLTextAreaElement).value) }}>Publish</button>
</div>
)
}
function EntryDisplay(props: { msg: string }) {
return (
<div className="entry">{props.msg}</div>
)
}
const reactRoot = document.getElementById("react-root")
ReactDOM.render(<App />, reactRoot)

State is updated correctly, the return keyword is missing here:
function Entries(props: { entries: string[] }) {
return (
<div className="entries">
{props.entries.map((v, i) => {
// add missing 'return'
return <EntryDisplay msg={v} key={v} />
})}
</div>
)
}
Also, don't use array indexes as keys.

Related

Modify class name of react element programmatically

Is there a native way to add a style class name to a react element passed as a property WITHOUT using jQuery or any 3rd-party libraries.
The following example should demonstrate what I'm trying to do. Note, react class names are made up.
Edit: The point is to modify the class name of a react element that is passes as a property to the Books class! The Books class needs to modify the class name. Apparently, it does not have access to Authors class's state to use within Authors class.
File authors.js
class Authors {
render() {
return (
<ul>
<li>John Doe</li>
<li>Jane Doe</li>
</ul>
);
}
}
File shelf.js
class Shelf {
render() {
return (
<div>
<Books authors={<Authors/>}/>
</div>
);
}
}
File books.js
class Books {
this.props.authors.addClass('style-class-name'); <- HERE
render() {
return (
<div>
{this.props.authors}
</div>
);
}
}
Potentially need more context, but in this kind of scenario, I would use state to dynamically add/remove a class. A basic example would be:
const App = () => {
const [dynamicClass, setDynamicClass] = useState("");
return (
<div className={`normalClass ${dynamicClass}`}>
<button onClick={() => setDynamicClass("red")}>Red</button>
<button onClick={() => setDynamicClass("green")}>Green</button>
<button onClick={() => setDynamicClass("")}>Reset</button>
</div>
);
};
The state changes schedule a re-render, hence you end up with dynamic classes depending on the state. Could also pass the class in as a property, or however you want to inject it into the component.
React elements do have an attribute called className. You can use that to set CSS classes to your component. You can pass static data (strings) or dynamic ones (basically calculated ones):
<div className="fixedValue" />
<div className={fromThisVariable} />
Keep in mind, that you have to pass down your className, if you wrap native HTML elements in a component:
class Books {
render() {
const {
authors,
// other represents all attributes, that are not 'authors'
...other
}
return (
<div {...other}>
{this.props.authors}
</div>
);
}
}
If you want to add data to your authors attribute (which I assume is an array), you could implement a thing like the following:
let configuredAuthors = this.props.authors.map((author) => ({
return {
...author,
className: `${author.firstName}-${author.lastName}`
}
}))
Keep in mind, that either way, you have to manually assign this className property in your child components (I guess an Author component)
To handle updates from a child component, use functions: https://reactjs.org/docs/faq-functions.html
Full example:
import React from "react";
class Shelf extends React.Component {
render() {
const authors = [
{
firstName: "John",
lastName: "Tolkien"
},
{
firstName: "Stephen",
lastName: "King"
}
];
return (
<div>
<Books authors={authors} />
</div>
);
}
}
const Books = ({authors, ...other}) => {
const [configuredAuthors, setAuthors] = React.useState(authors)
const updateClassName = (authorIndex, newClassName) => {
const newAuthors = [...configuredAuthors]
newAuthors[authorIndex] = {
...configuredAuthors[authorIndex],
className: newClassName
}
setAuthors(newAuthors)
}
return (
<ul {...other}>
{configuredAuthors.map((author, index) => {
return <Author key={index} author={author}
index={index}
updateClassName={updateClassName}
/>;
})}
</ul>
);
}
const Author = ({ author, index, updateClassName, ...other }) => {
const [count, setCount] = React.useState(0);
return (
<li className={author.className} {...other}>
<span>{`${author.firstName} ${author.lastName}`}</span>
<button
onClick={() => {
updateClassName(index, `author-${count}`);
setCount(count + 1);
}}
>
update Class ()
{`current: ${author.className || '<none>'}`}
</button>
</li>
);
};
export default function App() {
return <Shelf />;
}

React doesn't re-render components after state changes

I'm creating a todo app and I have some problem
I got a feature to add a todo item to favorite and so objects with "favorite: true" should be first at the array of all todos.
useEffect(() => {
// Sort array of objects with favorite true were first
setTodos(todos.sort((x, y) => Number(y.favorite) - Number(x.favorite)));
}, [todos]);
//Add to favorite function
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: e.favorite,
};
}
return e;
})
);
};
<div className="favorite-button" onClick={() => favoriteHandler()}>
{favorite ? (
<img src={FavoriteFilledIcon} alt="Remove from favorite" />
) : (
<img src={FavoriteIcon} alt="Add to favorite" />
)}
</div>
but if click on a favoriteHandler console log tells me that objects with favorite: true is at the start of array, but todos.map doesn't re-render this changes, why?
// App.js
{todos.map((e, i) => (
<TodoItem
completed={e.completed}
id={e.id}
key={i}
text={e.name}
setTodos={setTodos}
todos={todos}
favorite={e.favorite}
/>
))}
There is no re-render because no state is being change.
In favoriteHandler function you are just setting the favorite as the initial favorite you supplied as props. If you're trying to toggle favorite on click you should consider using boolean operator as below.
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: !e.favorite,
};
}
return e;
})
);
};
In terms of sorting you won't need to use useEffect hook but re-write as following:
todos
.sort((x, y) => Number(y.favorite) - Number(x.favorite))
.map((todo) => {
return (
<TodoItem
completed={todo.completed}
id={todo.id}
key={todo.id}
text={todo.text}
setTodos={setTodos}
todos={todos}
favorite={todo.favorite}
/>
);
});
For Improvement in your code I suggest the following.
Try to use a more straightforward variable name like todo and stay away from variables name like e.
Instead of attaching onClick on div tag, use button tags.
When you are trying to map over an array and have a unique key, you should use that key instead of index.

Using refs and .reduce scroll to the id of selected element with react with useState

sorry if the title doesn't make much sense.
I've been refactoring my code from this.state to useState, and I finally got things working except for the refs...
In my original code I was making individual axios calls and using this.state along with this refs code:
const refs = response.data.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
but now I refactored my axios call to .all:
const getData = () => {
const getSunSigns = axios.get(sunSignAPI);
const getDecans = axios.get(decanAPI);
const getNums = axios.get(numbersAPI);
axios.all([getSunSigns, getDecans, getNums, refs]).then(
axios.spread((...allData) => {
const allSunSigns = allData[0].data;
const getAllDecans = allData[1].data;
const getAllNums = allData[2].data;
setSunSigns(allSunSigns);
setDecanSign(getAllDecans);
setNumerology(getAllNums);
})
);
};
useEffect(() => {
getData();
}, []);
so the response.data.reduce doesn't work cuz I'm not using 'response'.
I've tried several things but none worked.. unfortunately I deleted all the previous code but this is what I currently have, which works but obviously only takes one api:
const refs = sunSigns.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
onClick = (id) => {
refs[id].current.scrollIntoView({
behavior: "smooth",
});
};
from the research I've done and the code I've tried I'm sure I'd have to map through the apis and then maybe use the reduce(???).. but I'm really not entirely sure how to go about it or how to rephrase my google search to get more accurate results.
what I'm trying to do specifically: on certain pages an extra nav bar appears with the symbol of a specific sign/number. the user can click on one and it'll scroll to that specific one. I'm going to have several pages with this kind of feature so I need to dynamically set refs for each api.
any help or guidance will be highly appreciated!!
edit**
the above codes are in my Main component and this is where I'm setting the refs:
return (
<div className='main'>
<div className='main__side-container'>
<SideBar />
<div className='main__card-container'>
<Card
sunSigns={sunSigns}
numerology={numerology}
decanSign={decanSign}
refs={refs}
/>
</div>
</div>
<div className='main__bottom-container'>
<BottomBar
sunSigns={sunSigns}
numerology={numerology}
onClick={onClick}
/>
</div>
</div>
);
}
then this is the card:
export default function Card({ sunSigns, decanSign, refs, numerology }) {
return (
<>
<div className='card'>
<Switch>
<Route path='/astrology/western/zodiac'
render={(routerProps) => <Zodiac refs={refs} sunSigns={sunSigns} />}
/>
<Route path='/numerology/pythagorean/numbers'
render={(routerProps) => <NumberPage refs={refs} numerology={numerology} />}
/>
</Switch>
</div>
</>
);
}
and then this is the Zodiac page:
export default function Zodiac({ sunSigns, refs }) {
return (
<>
<div className='zodiac__container'>
<TitleBar text='ZODIAC :' />
<div className='card-inset'>
<div className='container-scroll'>
<SunSignsList sunSigns={sunSigns} refs={refs} />
</div>
</div>
</div>
</>
);
}
and the SunSignsList component:
export default function SunSignsList({ sunSigns, refs }) {
return (
<>
<div className='sunsignsitemlist'>
<ul>
{sunSigns.map(sign => {
return (
<SunSigns
refs={refs}
key={sign.id}
id={sign.id}
sign={sign.sign}
/>
);
})}
</ul>
</div>
</>
);
}
and the SunSigns component:
export default function SunSigns({
id,
sign,
decans,
refs
}) {
return (
<li ref={refs[id]}>
<section className='sunsigns'>
<div className='sunsigns__container'>
<div className='sunsigns__header'>
<h3 className='sunsigns__title'>
{sign}
{decans}
</h3>
<h4 className='sunsigns__symbol'>{symbol}</h4>
</section>
</li>
);
}
the above code is where my ref code is currently accessing correctly. but the end goal is to use them throughout several pages and comps in the same manner.
You can create three different objects holding the ref data for each list or if the id is same you can generate a single object which holds all the list refs.
const generateAllRefsObj = (...args) => {
const genSingleListRefsObj = (acc, value) => {
acc[value.id] = createRef();
return acc;
}
return args.reduce((acc, arg) => ({ ...arg.reduce(genSingleListRefsObj, acc), ...acc }), {})
}
Usage
const allRefs = generateAllRefsObj(sunSigns,decanSign,numerology)

JSX Not Updating In Real Time When Checkboxes Checked In React

Attempting to do a recipe app, where once you check the boxes, the recipe array updates, and the counter at the bottom is updated as you check them. So far though, I have only been able to do one or the other, if I add the ingredient to the array, I can't update the counter, and if I update the counter, I cannot update the array. Here's what I have so far.
import React from 'react';
function IngredientsCheckBoxes(props) {
let newData = props.ingredients
let newestArray = []
let handleOnChange = (e) => {
let isChecked = e.target.checked;
if (isChecked) {
for ( let i = 0; i <= newestArray.length; i++ ){
if (e.target.value !== i) {
newestArray.push(e.target.value)
return newestArray
}
}
} else if (isChecked === false) {
newestArray = newestArray.filter(
ingred => ingred !== e.target.value
)
}
}
return (
<div>
<ul className="toppings-list">
{newData.map(( name , index) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
type="checkbox"
id={`custom-checkbox-${index}`}
name={name}
value={name}
onChange={e => handleOnChange(e)}
/>
<label htmlFor={`custom-checkbox-${index}`}>{name}</label>
</div>
</div>
</li>
);
})}
</ul>
<h1>{count goes here}</h1>
</div>
)
}
export default IngredientsCheckBoxes;
I previously used a useState hook, but where ever I put it in the handleOnChange function, it takes over and won't put the ingredient in the array.
I have a feeling the answer is super obvious but after looking at the code for a while I'm looking for outside help, any of which would be appreciated. Thank you in advance.
You're supposed to use state to keep track of what's changing inside your component.
Also,
you should avoid using array index as a key
and would be much better off addressing your items by unique id (name) rather than rely on their position inside array (that's being constantly changed)
and drop dynamically generated id's, as odds of never using those for referring to specific DOM node are quite high
breaking your UI into more granular (reusable) components would be good for your app
Distilled example of what you're (seemingly) trying to achieve, may look as follows:
const { useState } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const CheckListItem = ({onHit, label}) => {
return (
<label>
<input type="checkbox" onChange={() => onHit(label)} />
{label}
</label>
)
}
const CheckList = ({itemList}) => {
const [selectedItems, setSelectedItems] = useState(
itemList
.reduce((acc, itemName) =>
(acc[itemName] = false, acc), {})
)
const onCheckItem = itemName =>
setSelectedItems({...selectedItems, [itemName]: !selectedItems[itemName]})
const numberOfChosenItems = Object
.values(selectedItems)
.filter(Boolean)
.length
const listOfChosenItems = Object
.keys(selectedItems)
.filter(itemName => selectedItems[itemName] === true)
.join(', ')
return !!itemList.length && (
<div>
<ul>
{
itemList.map(itemName => (
<li key={itemName}>
<CheckListItem
label={itemName}
onHit={onCheckItem}
/>
</li>
))
}
</ul>
{
!!numberOfChosenItems && (
<div>
Chosen items: {listOfChosenItems} (Total: {numberOfChosenItems})
</div>
)
}
</div>
)
}
const ingredients = ['flour', 'eggs', 'milk', 'sugar']
const App = () => {
return (
<CheckList itemList={ingredients} />
)
}
render (
<App />,
rootNode
)
li {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
newestArray should be a state in order to re-render IngredientsCheckBoxes component and subsequently show changes in the component view.
const [selectedIngredients, setSelectedIngredients] = useState([]);
And then use selectedIngredients and setSelectedIngredients to use and update state respectively.
Here's a codesandbox with the working code.
Make sure you use suitable names for state. selectedIngredients is a better name than newestArray as it does tell you about what it actually means.

Can you render a div from a function in React?

I'm trying to use the function below (renderMatchedLogs) to render values from the object it receives, and I'm able to console.log the values but nothing displays on the screen.
I thought JSX can be rendered on the screen from another function? But I'm not sure if this something I misinterpreted or if my logic is off.
Further details of the code:
In the render() {} portion of the code:
<button onClick={this.findMatches}>Find Matches</button>
Which triggers this function to find matches:
findMatches = () => {
const foodLog = this.state.foodLog;
const foodFilter = this.state.foodFilter;
console.log("food filter", foodFilter);
Object.keys(foodLog).map((key, index) => {
if (foodLog[key].foodSelectedKey.some((r) => foodFilter.includes(r))) {
const matchedLog = foodLog[key];
this.renderMatchedLogs(matchedLog);
} else {
// do nothing
}
});
};
And then this is the function to render the values:
renderMatchedLogs = (matchedLog) => {
return (
<div>
{matchedLog.dateKey}
<br />
{matchedLog.mealKey}
<br />
{matchedLog.foodSelectedKey}
<br />
{matchedLog.reactionKey}
<br />
</div>
);
};
You’re rendering it, but not telling the application where to put it. I’d recommend putting the matchedLogs items in state somewhere that you update when you call findMatches, and then within your actual component have a something that looks like this
<div>
{matchedLogs && (renderMatchedLogs())}
<div>
Which can be the same as you have, apart from it’ll read the actual data from the state and render it rather than doing all of that itself (as I’m seeing from this context you want that to be user triggered).
May be what you're looking for is something like this?
state = {
logs: null
}
renderMatchedLogs = (matchedLog) => {
return (
<div>
{matchedLog.dateKey}
<br />
{matchedLog.mealKey}
<br />
{matchedLog.foodSelectedKey}
<br />
{matchedLog.reactionKey}
<br />
</div>
);
};
findMatches = () => {
const foodLog = this.state.foodLog;
const foodFilter = this.state.foodFilter;
console.log("food filter", foodFilter);
const matchedLogs = [];
//use forEach instead to push to matchedLogs variable
Object.keys(foodLog).forEach((key, index) => {
if (foodLog[key].foodSelectedKey.some((r) => foodFilter.includes(r))) {
const matchedLog = foodLog[key];
// this will return the div element from renderMatchedLogs
matchedLogs.push(this.renderMatchedLogs(matchedLog));
}
});
const logs = (<>
{matchedLogs.map(div => div)}
</>);
this.setState({
logs
})
};
render(){
return (
<>
{this.state.logs}
<button onClick={this.findMatches}>Find Matches</button>
</>
)
}

Resources