React play different audio files at once - working with different refs - javascript

I'm creating a small app that plays an audio file and have some functionalities (loop, stop, mute). My goal is to add some more audio files that all should be played and stopped at once (one button to control all), but each will have a mute button, and I'm not sure what is the best practice to do so. I used useRef and thought maybe I need to set a refs array but how will I be able to start/stop them all at once, but still have the ability to control the mute separately?
This is my code so far. I guess I should split and have a different component for the audio sounds. Thanks for helping!
import React, {useState, useRef, useEffect} from 'react'
import {ImPlay2} from "react-icons/im"
import {ImStop} from "react-icons/im"
import styled from "styled-components"
import drums from '../loopfiles/DRUMS.mp3'
//import other audio files//
const AudioPlayer = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [isLooping, setIsLooping] = useState(false);
const [isOnMute, setIsOnMute] = useState(false);
const audioRef = useRef(new Audio(drums));
useEffect(() => {
if (isOnMute) {
audioRef.current.volume=0;
}
else {
audioRef.current.volume=1;
}
}, [isOnMute]);
useEffect(() => {
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
audioRef.current.load();
}
}, [isPlaying]);
useEffect(() => {
if (isLooping) {
audioRef.current.loop = true;
} else {
audioRef.current.loop = false;
}
}, [isLooping]);
return (
<div>
{!isPlaying ? (
<button type="button"
className="play"
onClick={() => setIsPlaying(true)}>
<ImPlay2></ImPlay2> Play
</button>
) : (
<button type="button"
className="pause"
onClick={() => setIsPlaying(false)}>
<ImStop></ImStop> Stop
</button>
)}
<Flex>
<Switcher selected={isLooping} />
<Text
onClick={() => setIsLooping(true)}>
Loop
</Text>
<Text
onClick={() => setIsLooping(false)}>
Unloop
</Text>
</Flex>
<Flex>
<Switcher selected={isOnMute} />
<Text
onClick={() => setIsOnMute(true)}>
Mute
</Text>
<Text
onClick={() => setIsOnMute(false)}>
UnMute
</Text>
</Flex>
</div>
)
}
const Flex = styled.div`
margin-top: 5px;
display: flex;
align-items: center;
border-radius: 2px;
background: grey;
height: 20px;
width: 120px;
position: relative;
margin-bottom: 5px;
`;
const Switcher = styled.div`
background: black;
border-radius: 2px;
height: 20px;
line-height: 41px;
width: 50%;
cursor: pointer;
position: absolute;
transition: 0.5s;
-webkit-transition: 0.5s;
-moz-transition: 0.5s;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
z-index: 1;
left: ${({ selected }) =>
selected === true ? "0px" : "60px"};
`;
const Text = styled.div`
color: ${({ selected }) => (selected ? "black" : "white")};
font-size: 13px;
font-weight: 20;
line-height: 4px;
padding: 30;
width: 50%;
text-align: center;
cursor: pointer;
`;
export default AudioPlayer

If you would like to mute/unmute individual sounds, but play/pause all sounds together, then you will need to create a mute/unmute slider for each sound. I can think of a number of ways to do this. The "best choice" might depend upon the standards in the rest of the application, how many sounds you're importing, and whether they're likely to change.
Method 1: One way to do this would be creating one array containing isOnMute values for each sound and another array containing all refs, and then map(...) over each of the elements of the isOnMute array to create your sliders.
Method 2: Another way would be to have one array of objects containing all sounds, and then the ref and the isOnMute values could be stored within each object. You could map(...) over that to create your sliders as well.
Method 3: You could also create separate child components for each sound like you said, and then pass the mute property between the parent AudioPlayer and the child AudioChannel components.
Then anytime the play/pause button is clicked, you would need to update each of the refs in the array (via a forEach or each of the child components via toggling a single isPlaying property).
Regardless of which you choose, I also might like to recommend the use-sound npm package. It makes managing multiple sounds and their properties a little bit less cumbersome in my opinion, including the ability to play and pause with a single method call.

Here is a snippet for you/
Also do not forget to use according ids instead of idx and idx2
const AudioList = () => {
/* here populate the array in format: array of objects
{
drums: mp3file,
isPlaying: boolean,
setIsPlaying: boolean,
isLooping: boolean,
setIsLooping: boolean,
isOnMute: boolean,
setIsOnMute: boolean,
}[]
*/
const [audios, setAudios] = useState([
{ isPlaying: true, isOnMute: false, isLooping: true, drums: "Your mpr" },
]); // use initial audios
return (
<div>
<button
onClick={() => {
// similar to start all, mute all, you have full controll logic over all elements
// also you could implement add new audiofile, or delete, similar logic :)
setAudios((audios) =>
audios.map((audio) => ({ ...audio, isPlaying: false }))
);
}}
>
Stop all
</button>
<div>
{audios.map((audio, idx) => (
<AudioPlayer
key={idx}
{...audio}
setIsPlaying={(val) =>
setAudios((audios) =>
audios.map((audio, idx2) =>
idx === idx2 ? { ...audio, isPlaying: val } : audio
)
)
}
// similar for setMute and setLopping function,
// i think you can figure it out, it is just a snippet:)
/>
))}
</div>
</div>
);
};
const AudioPlayer = ({
drums,
isPlaying,
setIsPlaying,
isLooping,
setIsLooping,
isOnMute,
setIsOnMute,
}) => {
const audioRef = useRef(new Audio(drums));
// also you have full controll of element inside component
useEffect(() => {
if (isOnMute) {
audioRef.current.volume = 0;
} else {
audioRef.current.volume = 1;
}
}, [isOnMute]);
useEffect(() => {
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
audioRef.current.load();
}
}, [isPlaying]);
useEffect(() => {
if (isLooping) {
audioRef.current.loop = true;
} else {
audioRef.current.loop = false;
}
}, [isLooping]);
return (
<div>
{!isPlaying ? (
<button
type="button"
className="play"
onClick={() => setIsPlaying(true)}
>
<ImPlay2></ImPlay2> Play
</button>
) : (
<button
type="button"
className="pause"
onClick={() => setIsPlaying(false)}
>
<ImStop></ImStop> Stop
</button>
)}
<Flex>
<Switcher selected={isLooping} />
<Text onClick={() => setIsLooping(true)}>Loop</Text>
<Text onClick={() => setIsLooping(false)}>Unloop</Text>
</Flex>
<Flex>
<Switcher selected={isOnMute} />
<Text onClick={() => setIsOnMute(true)}>Mute</Text>
<Text onClick={() => setIsOnMute(false)}>UnMute</Text>
</Flex>
</div>
);
};

I changed the following:
I added Audios.js containing:
const audios = () => {
return [
{
color: 'lightgreen',
isOnMute: false,
audio: drums,
title: 'Drums'
}, ...
AudioList.js:
const AudioList = ({isPlaying, isLooping}) => {
const [audioToPlay, setAudioToPlay] = useState();
useEffect(()=> {
setAudioToPlay(audios())
},[]) ....//and mapped through <AudioItem>:
AudioItem.js:
const AudioItem = ({audio, isPlaying, isLooping}) => {
const [isOnMute, setIsOnMute] = useState(false);
const audioRef = useRef(null);
useEffect(() => {
if (isLooping) {
audioRef.current.loop = true;
} else {.... //other functionality
added a progressBar.js:
const ProgressBar = ({isPlaying}) => {
const [completed, setCompleted] = useState({
count: 0
});const intervalId = useRef(null)
useEffect(() => {...
ControlPanel.js:
const ControlPanel = ({
setIsLooping, isLooping, isPlaying, setIsPlaying}) => {
return (
<div>
<PlayButton> //....
and Home.js containing controlpanel, AudioList, ProgressBar:
const Home = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [isLooping, setIsLooping] = useState(false);
return (
<div>
<ControlPanel
setIsLooping={setIsLooping} //....

Related

how to pin a note and sort it in an array

I was creating a notes app in react which has a pin functionality such that when I click on the pin icon on a particular note that particular note is displayed first. the user should only be able to pin 2 notes (and I've added that condition) the problem is The pin functionality is working perfectly fine but when I again click on a pinned note I want to un-pin it and again arrange it back in its place, how can i achieve this?
React code =
import React, { useState } from "react";
import "../styles/Notes.css";
import { useToast, Wrap, WrapItem, Button } from '#chakra-ui/react'
import { BsFillPinFill } from "react-icons/bs"
import { BsTrashFill } from "react-icons/bs"
import { BsPinAngle } from "react-icons/bs";
function NotesComponent() {
const [notes, setNotes] = useState([]);
const [title, setTitle] = useState("");
const [tagline, setTagline] = useState("");
const [body, setBody] = useState("");
const [page, setPage] = useState(1);
const toast = useToast()
// submit handler
const handleSubmit = (event) => {
event.preventDefault();
if (!title || !tagline || !body) {
toast({
title: 'Please complete the input',
status: 'error',
duration: 9000,
isClosable: true,
});
return;
}
// generating randome number to use as id
function generateUniqueNumber() {
let uniqueNumber = "";
while (uniqueNumber.length < 4) {
let digit = Math.floor(Math.random() * 10);
if (!uniqueNumber.includes(digit)) {
uniqueNumber += digit;
}
}
return uniqueNumber;
}
let number = generateUniqueNumber();
setNotes([...notes, { title, tagline, body, pinned: false, id: number }]);
setTitle("");
setTagline("");
setBody("");
};
// executing on click on the pen icon
const togglePin = (index) => {
setNotes(
notes.map((note, i) => {
if (i === index) {
let newNote = { ...note };
newNote.pinned = !note.pinned;
return newNote;
}
return note;
})
);
};
// sorting it
const sortedNotes = notes.sort((a, b) => {
if (a.pinned === b.pinned) {
return 0;
}
return a.pinned ? -1 : 1;
})
.map((note, i) => {
let newNote = { ...note };
if (note.pinned) {
const pinnedCount = notes.filter((n) => n.pinned).length;
if (pinnedCount > 2) {
newNote.pinned = false;
}
}
return newNote;
});
const pages = [1, 2, 3, 4, 5, 6];
const pageChnageHandler = (e) => {
setPage(e.target.innerText);
};
const deleteHandler = (id) => {
let index = id
const newArrayAfterDeleting = notes.filter((item) => item.id !== index)
setNotes(newArrayAfterDeleting)
}
return (
<div className="notes-app-container">
<form onSubmit={handleSubmit} className="notes-form">
<input
type="text"
placeholder="Title"
value={title}
onChange={(event) => setTitle(event.target.value)}
className="notes-input"
/>
<input
type="text"
placeholder="Tagline"
value={tagline}
onChange={(event) => setTagline(event.target.value)}
className="notes-input"
/>
<textarea
placeholder="Body"
value={body}
onChange={(event) => setBody(event.target.value)}
className="notes-textarea"
/>
<button type="submit" className="notes-button">
Add Note
</button>
</form>
<div className="enteredNotesMainParent">
{sortedNotes.slice(page * 6 - 6, page * 6).map((note, i) => (
<div key={i} className="enteredNoteIndivitual">
<div>{note.title}</div>
<div>{note.tagline}</div>
<div>{note.body}</div>
<br />
<div className="noteCtaHold">
<div>
<BsFillPinFill className="noteIcon" onClick={() => togglePin(i)} />
</div>
<div>
<BsTrashFill className="noteIcon" onClick={() => deleteHandler(note.id)} />
</div>
</div>
</div>
))}
</div>
{notes.length === 0 ? <p> Add some notesâś… </p> : ""}
{notes.length >= 4 && <div className="pagesHold">
{pages.map((item) => {
return <p onClick={pageChnageHandler} className="indivitualPage"> {item} </p>
})}
</div>}
</div>
);
}
export default NotesComponent;
can somebody please help me achieve this that if a note is pinned and if I click on that pinned note it should get un-pinned and re arrange back
You don't necessarily have to use sort() to get the pinned notes on top.
Just render the list twice: once for the pinned notes, filtering out the unpinned ones, and again for the rest of the list, filtering the pinned ones.
This way you don't have to concern yourself with where a given note is within the original list, because the original list doesn't change.
// creates a list of sample notes; not relevant to the funcionality.
const notes = Array.from({length: 6}, (_, i) => ({
title: `Note ${i + 1}`,
id: i
}))
function Notes ({notes}) {
// keep a list of the pinned note ids
const [pinned, setPinned] = React.useState([]);
// filter to get separate lists of pinned and unpinned notes
const pinnedNotes = notes.filter(({ id }) => pinned.includes(id));
const unpinnedNotes = notes.filter(({ id }) => !pinned.includes(id));
// to pin a note: add its id to the pinned list
const pin = id => setPinned([...pinned, id]);
// to unpin a note: remove its id from the pinned list
const unpin = id => {
pinned.splice(pinned.indexOf(id), 1);
setPinned([...pinned]);
}
// render both lists
return (
<div className="container">
<ul className="pinned">
{ pinnedNotes.map(note => (
<li key={note.id} onClick={() => unpin(note.id)}>{note.title}</li>
))}
</ul>
<ul className="unpinned">
{ unpinnedNotes.map(note => (
<li key={note.id} onClick={() => pin(note.id)}>{note.title}</li>
))}
</ul>
</div>
)
}
const root = ReactDOM.render(<Notes notes={notes} />, document.getElementById('root'));
/* all cosmetic. not necessary for it to work. */
.container {
font-family: sans-serif;
font-size: 12px;
}
.pinned {
background: skyblue;
margin-bottom: 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5em;
padding: 0 0.5em;
}
.pinned:empty::after {
content: "No pinned items. Click a note below to pin it.";
padding: 1rem;
display: block;
text-align: center;
}
.unpinned:empty::after {
content: "No unpinned notes.";
padding: 1rem;
display: block;
text-align: center;
}
.pinned li {
background: aliceblue;
}
.unpinned {
background: aliceblue;
}
li {
margin: 0.5em 0;
padding: 1em;
border: 1px solid steelblue;
border-radius: 2px;
}
ul {
list-style: none;
margin: 0;
padding: 0.25em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Creating a javascript/react transition for expanded

I'm trying to create a transition that will expand this over half a second. I'm not sure how to go about this I tried attaching css but no use. Any suggestions?
const getFullMetricList = () =>
Object.keys(comparisonChartsKeyMap)
.map((category) => Object.values(comparisonChartsKeyMap[category]))
.flat();
const MetricCatalog = ({ AddMetric, DeleteMetric, Metrics, width, height }) => {
const [expanded, setExpanded] = useState(false);
const [filteredMetricList, setFilteredMetricList] = useState(
getFullMetricList()
);
const handleFuzzyFilterItems = (value) => {
let newConf = matchSorter(getFullMetricList(), value);
setFilteredMetricList(newConf);
};
const handleCloseCatalog = () => {
setExpanded(false);
setFilteredMetricList(getFullMetricList());
};
if (!expanded) {
return (
<div
className='metric-catalog-btn-container'
onClick={() => setExpanded(!expanded)}
>
Metric Catalog
</div>
);
}
import { useState } from "react";
import "./styles.css";
export default function App() {
const [expanded, setExpanded] = useState(false);
return (
<div
className={expanded ? "expanded" : ""}
onClick={() => setExpanded(!expanded)}
>
hello
</div>
);
}
div {
width: 100px;
height: 100px;
background: red;
transition: width 0.5s;
}
.expanded {
width: 300px;
}
complete code link

change styles dynamically with react memo

I am learning to use react memo, I am applying it to a simple task application, my problem is that I do not know how to make the styles of the items in the list work well.
As you can see when completing a task, I don't know how to update the styles of the other items in the list so that it is one in white and one in black.
I tried many things but nothing worked :(
TaskItem.jsx
import React, { memo } from 'react'
import styled from "styled-components"
import { useSelector } from "react-redux";
import store from "../../redux/store";
//STYLES
const DIV = styled.div`
max-height: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px" : "50px"
};
opacity: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0": "1"
};
padding: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px":"12px 15px"
};
overflow: hidden;
transition: opacity 0.5s, max-height 0.5s, padding 0.5s;
`;
const TR = styled.tr`
background-color: ${
(props) => {
//show completed and not completed tasks
if(useSelector((state)=> state.toggleDoneTasks.show)){
return props.index % 2 === 0 ? '#f3f3f3': 'none'
}
const tasksNotDone = props.tasks.filter((task) => !task.done)
const index = tasksNotDone.findIndex(t => t.id === props.task.id)
return index % 2 === 0 ? '#f3f3f3': 'none'
}
};
/*
&:nth-child(even) {background: #CCC};
&:nth-child(odd) {background: #FFF};
*/
border-bottom: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.task.done ? "none": "1px solid #dddddd"
};;
transition: visibility 0.5s;
cursor: pointer;
&:hover{
font-weight: bold;
color: #009879;
}
`;
function TaskRow({ task, toggleDoneTask, index, tasks }) {
return (
<TR task={task} tasks={tasks} index={index}>
<td>
<DIV done={task.done}>
{console.log('render', task)}
{task.title}
</DIV>
</td>
<td>
<DIV done={task.done}>
{task.description}
</DIV>
</td>
<td>
<DIV done={task.done}>
<input type="checkbox"
checked={task.done}
onChange={toggleDoneTask}
style={{cursor: 'pointer'}}
/>
</DIV>
</td>
</TR>
)
}
export default memo(TaskRow, (prev, next) => {
// store.getState().toggleDoneTasks.show
//COMPARE TASK OBJECT
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
return sameLength && sameEntries;
})
Tasks.jsx
import React, { useEffect, useReducer } from "react";
import TaskItem from "./TaskItem";
function saveLocalStorage(tasks) {
localStorage.setItem("tasks", JSON.stringify(tasks));
}
function TasksReducer(taskItems, { type, task }) {
switch (type) {
case "UPDATE_TAKS": {
let taskItemsCopy = [...taskItems].map((task) => ({ ...task }));
let newItems = taskItemsCopy.map((t) => {
if (t.id === task.id) {
t.done = !t.done;
}
return t;
});
saveLocalStorage(newItems);
return newItems;
}
case "ADD_TASK": {
const newItems = [...taskItems, task];
saveLocalStorage(newItems);
return newItems;
}
default:
window.alert("INVALID ACTION");
break;
}
}
const initialState = JSON.parse(localStorage.getItem("tasks")) || [];
//STYLES
const styleTable = {
borderCollapse: "collapse",
margin: "25px 0",
fontSize: "0.9em",
fontFamily: "sans-serif",
minWidth: "400px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)"
};
const styleTr = {
backgroundColor: "#009879",
color: "#ffffff",
textAlign: "left"
};
const styleTh = {
padding: "12px 15px"
};
function Tasks({ newTask, show }) {
const [taskItems, dispatch] = useReducer(TasksReducer, initialState);
useEffect(() => {
if (!newTask) return;
newTaskHandler({ id: taskItems.length + 1, ...newTask });
}, [newTask]);
const newTaskHandler = (task) => {
dispatch({ type: "ADD_TASK", task });
};
const toggleDoneTask = (task) => {
dispatch({ type: "UPDATE_TAKS", task });
};
return (
<React.Fragment>
<h1>learning react </h1>
<table style={styleTable}>
<thead>
<tr style={styleTr}>
<th style={styleTh}>Title</th>
<th style={styleTh}>Description</th>
<th style={styleTh}>Done</th>
</tr>
</thead>
<tbody>
{taskItems.map(
(task, i) =>
(show || !task.done) && (
<TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}
/>
)
)}
</tbody>
</table>
</React.Fragment>
);
}
export default Tasks;
If you need the complete code:
About Memo and your app
Memo could be useful when you don't expect your component to change often. It comes with a cost that does these evaluations to verify if it should rerender.
In your case you would have a lot of rerenders because several components would need to be rerender to fit the correct background which is a downside to use Memo.
Besides, you would see these bugs, since other components won't rerender given their props won't change.
I would suggest to remove memo for this case.
About your show state
You see the accordion effect that you have right? You wouldn't see that effect if the component did unmount. That means the component never unmounts. You are using 2 show states, one state created at root at your App and another state that comes from your reducer.
Your button only flips the reducer show state. But to render TaskItem you use the stale show state created, that is always true. If you were using the redux state there wouldn't be any effect at all:
// show is always true unless you pass state from your reducer like:
// const show = useSelector((state) => state.toggleDoneTasks.show);
(show || !task.done) && (
<TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}
/>
)
so you should remove your React show state:
const [show, setShow] = useState(JSON.parse(localStorage.getItem('show')) || true)
Or remove your reducer, but for learning purposes you can keep the reducer.
Sandbox without the extra state, and no effects:
About the accordion effect
Given that you won't have anymore the accordion effect. For you to solve that you can either:
pick up a react animation library that handles transition effects on component mounting and unmounting of your preference;
render all Tasks (without (show || !task.done) condition). And keep track at each Task how many tasks are done until that given Task. With that you can do some logic like:
const indexToPass = show ? index : index - doneTasksBefore
...
<TR task={task} tasks={tasks} index={indexToPass}>

How to use each table row with different background color

How I use each table row with different background color like nth-child (even) and nth-child (odd), at first it works as I want, but then when I show only uncompleted tasks it doesn't work
also when I complete a task where only incomplete tasks are shown this is not updated
I am also trying to avoid rendering all tasks, I tried many things but nothing works and I already run out of ideas
Tasks Component:
import React, { useEffect, useReducer } from 'react'
import TaskItem from "./TaskItem";
function saveLocalStorage(tasks) {
localStorage.setItem('tasks', JSON.stringify(tasks))
}
function TasksReducer(taskItems, { type, task }) {
switch (type) {
case 'UPDATE_TAKS': {
let taskItemsCopy = [...taskItems].map((task) => ({ ...task }))
let newItems = taskItemsCopy.map((t) => {
if (t.id === task.id) {
t.done = !t.done
};
return t;
})
saveLocalStorage(newItems)
return newItems
}
case 'ADD_TASK': {
const newItems = [...taskItems, task]
saveLocalStorage(newItems)
return newItems
}
default:
window.alert('INVALID ACTION')
break;
}
}
const initialState = JSON.parse(localStorage.getItem('tasks')) || []
//STYLES
const styleTable = {
'borderCollapse': 'collapse',
'margin': '25px 0',
'fontSize': '0.9em',
'fontFamily': 'sans-serif',
'minWidth': '400px',
'boxShadow': '0 0 20px rgba(0, 0, 0, 0.15)'
}
const styleTr = {
'backgroundColor': '#009879',
'color': '#ffffff',
'textAlign': 'left'
}
const styleTh = {
padding: '12px 15px'
}
function Tasks({ newTask, show }) {
const [taskItems, dispatch] = useReducer(TasksReducer, initialState);
useEffect(() => {
if (!newTask) return
newTaskHandler({ id: taskItems.length + 1, ...newTask })
}, [newTask])
const newTaskHandler = (task) => {
dispatch({ type: 'ADD_TASK', task })
}
const toggleDoneTask = (task) => {
dispatch({ type: 'UPDATE_TAKS', task })
}
return (
<React.Fragment>
<h1>learning react </h1>
<table style={styleTable}>
<thead>
<tr style={styleTr}>
<th style={styleTh}>Title</th>
<th style={styleTh}>Description</th>
<th style={styleTh}>Done</th>
</tr>
</thead>
<tbody>
{
taskItems.map((task, i) => {
return <TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}>
</TaskItem>
})
}
</tbody>
</table>
</React.Fragment>
)
}
export default Tasks
Task Item component:
import React, { createRef, memo } from 'react'
import styled from "styled-components"
import { useSelector } from "react-redux";
import store from "../../redux/store";
//STYLES
const DIV = styled.div`
max-height: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px" : "50px"
};
opacity: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0": "1"
};
padding: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px":"12px 15px"
};
overflow: hidden;
transition: opacity 0.5s, max-height 0.5s, padding 0.5s;
`;
/*
avoid re-rendering all tasks when changing show state
that's why it's made this way
============================================
*/
const TR = styled.tr`
background-color: ${
(props) => {
//show completed and not completed tasks
if(useSelector((state)=> state.toggleDoneTasks.show)){
return props.index % 2 === 0 ? '#f3f3f3': 'none'
}
//show only not completed tasks ( FIX IT )
const tasksNotDone = props.tasks.filter((task) => !task.done)
const index = tasksNotDone.findIndex(t => t.id === props.task.id)
console.log(tasksNotDone)
console.log(index)
return index % 2 === 0 ? '#f3f3f3': 'none'
}
};
/* &:nth-child(even) {background: #CCC}
&:nth-child(odd) {background: #FFF} */
/*
avoid re-rendering all tasks when changing show state
that's why it's made this way
============================================
*/
border-bottom: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.task.done ? "none": "1px solid #dddddd"
};;
transition: visibility 0.5s;
cursor: pointer;
&:hover{
font-weight: bold;
color: #009879;
}
`;
function TaskRow({ task, toggleDoneTask, index, tasks }) {
return (
<TR task={task} tasks={tasks} index={index}>
<td>
<DIV done={task.done}>
{console.log('render', task)}
{task.title}
</DIV>
</td>
<td>
<DIV done={task.done}>
{task.description}
</DIV>
</td>
<td>
<DIV done={task.done}>
<input type="checkbox"
checked={task.done}
onChange={toggleDoneTask}
style={{cursor: 'pointer'}}
/>
</DIV>
</td>
</TR>
)
}
export default memo(TaskRow, (prev, next) => {
//COMPARE TASK OBJECT
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
// store.getState().toggleDoneTasks.show
return sameLength && sameEntries;
})
If you need the complete code
As suggested, you could try not rendering the tasks that are already done when they should be hidden. For example in your map callback you could try doing something like this:
{taskItems.map((task, i) => ((show || !task.done) &&
<TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}
/>
)}
The logic here: (show || !task.done) && <TaskItem /> is basically saying, "If we want to show all tasks OR the current task is not done, show the TaskItem".
This also requires cleaning up your complex styled components to use CSS instead of calculating the nth-child based on index via JS.
Can be done using &:nth-child(odd) (or similar).
In addition, you are using a combination of native react's useState and react-redux useSelector for your show prop/state value. I would recommend using react-redux as that seems to be what you are using in most of your code. To do that change your Tasks component to use useSelector, e.g.
function Tasks({ newTask }) {
const show = useSelector((state) => state.toggleDoneTasks.show);
...
I forked your code with a minimal working example here: https://codesandbox.io/s/recursing-pascal-vjst8?file=/src/components/Tasks/TaskItem.jsx may need some more tweaks. Hope it helps.

React Context values not referenced correctly in dynamic element functions

I created a Context and hook to be able to see if areas of the application has been changed, and validate actions based on the current state in context.
Its called DirtyContext and the Implementation is used as follows:
const {isDirty, setDirtyContextFor} = useDirtyContext();
setDirtyContextFor(key) - Ads a key to a list, to mark something as dirty.
isDirty - Reports the current state of the application based on a memoized value that updates everytime something is removed or added to the list of keys.
I have a list of objects, that helps me create a set of dynamic elements on the page.
const thisFunctionWillLooseContextReference = (e) => {
e.preventDefault();
console.log('Context Value - IsDirty: ', isDirty)
};
const [buttons, setButtons] = useState(() => {
return [{onClick: thisFunctionWillLooseContextReference}]
});
This is tied together in the UI using the following:
const renderButtons = () => {
return buttons.map((btn, index) => (
<button onClick={btn.onClick}>Button-{index}</button>
));
}
Even if the context value isDirty is set to true, the function passed to the button, always just reports the initial value of isDirty.
Would appreciate any help on why this is happening, and how i can get the expected results (which is the correct/current value of isDirty)
Codepen - Have a look at the console when clicking the buttons:
(Code and implementation details are reduced to the smallest reproducable state)
const { useState, useMemo } = React;
const DirtyContext = React.createContext();
const DirtyContextProvider = ({ children }) => {
const [dirtyList, setDirtyList] = useState(new Set());
const isDirty = useMemo(() => {
return dirtyList.size > 0;
}, [dirtyList]);
function setDirtyStateFor(componentName) {
const newDirtyList = new Set(dirtyList);
newDirtyList.add(componentName);
setDirtyList(newDirtyList);
}
return (<DirtyContext.Provider value={{
setDirtyStateFor,
isDirty,
}}>
{children}
</DirtyContext.Provider>);
};
const useDirtyContext = () => React.useContext(DirtyContext);
const MyDirtyLittleApp = () => {
const {isDirty, setDirtyStateFor} = useDirtyContext();
const [input, setValue] = useState("");
const thisFunctionWillLooseContextReference = (e) => {
e.preventDefault();
console.log('Context Value - IsDirty: ', isDirty)
};
const [buttons, setButtons] = useState(() => {
return [{onClick: thisFunctionWillLooseContextReference}]
});
function handleInput(event) {
setValue(event.target.value);
setDirtyStateFor('MyDirtyLittleApp');
}
function updateInput(event) {
event.preventDefault();
console.log('Am i dirty ?', isDirty)
}
const renderButtons = () => {
return buttons.map((btn, index) => (
<button class="button is-dark" data-reactstuff={isDirty} onClick={btn.onClick}>btn {index}</button>
));
}
return (
<React.Fragment>
<h1>{isDirty ? 'I`m Dirty': 'I`m Clean'}</h1>
<form className="form">
<div class="field">
<label for="name-1">Update DirtyContext</label>
<div class="control">
<input type="text" value={input} name="name-1" onChange={handleInput} class="input"/>
</div>
</div>
<div class="field">
<div class="control">
<button onClick={updateInput} class="button is-dark">Save</button>
{renderButtons()}
</div>
<control>
<h5>Check console for results when clicking on the buttons</h5>
</control>
</div>
</form>
</React.Fragment>
)
}
const App = () => {
return (
<DirtyContextProvider>
<div className="box">
<MyDirtyLittleApp />
</div>
</DirtyContextProvider>
)
}
ReactDOM.render(<App />,
document.getElementById("root"))
body {
height: 100vh;
margin: 0;
display: grid;
place-items: center;
}
.box {
width: 300px;
h1 {
font-size: 20px;
margin: 0 0 1rem 0;
}
h5 {
font-size: 12px;
}
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" type="text/css" />
<script src="https://unpkg.com/react#16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
https://codepen.io/Cnordbo/pen/zYqwVRL

Categories

Resources