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}>
Related
I'm trying to add an active class to some buttons in react but they're not applying. I'm not getting any errors or warnings and the syntax seems correct. What am i missing?
I have some handleEvent functions in JSX, I was expecting the "active-work" class to be applied to the span within the "work__filters" div. Everything else seems to be working. Code:
export default Works
part of CSS not getting applied:
import React, { useEffect, useState } from 'react'
import { projectsData } from './Data'
import { projectsNav } from './Data'
import WorkItems from './WorkItems';
import "./work.css";
const Works = () => {
const [item, setItem] = useState({ name: 'all'});
const [projects, setProjects] = useState([]);
const [active, setActive] = useState(0);
useEffect(() => {
if(item.name === "all") {
setProjects(projectsData);
}
else {
const newProjects = projectsData.filter((project) => {
return project.category.toLowerCase() === item.name;
});
setProjects(newProjects);
}
}, [item]);
const handleClick = (e, index) => {
setItem({ name: e.target.textContent.toLowerCase()});
setActive(index);
};
return (
<div>
<div className="work__filters">
{projectsNav.map((item, index) => {
return (
<span
onClick={(e) => {
handleClick(e, index);
}}
className={`${active === index ? 'active-work' : ""}work__item`}
key={index}>
{item.name}
</span>
);
})}
</div>
<div className="work__container container grid">
{projects.map((item) => {
return <WorkItems item={item} key={item.id}/>
})}
</div>
</div>
);
}
export default Works
//CSS not being applied
.active-work {
background-color: black;
color: white;
}
The code looks fine at first glance, except you need to put a space between work__item and the condition. You should see with the Devtools that the class is getting applied, but it will probably look like class="active-workwork__item", hence the styling is not applied.
https://codesandbox.io/embed/sleepy-bird-975b1m?fontsize=14&hidenavigation=1&theme=dark
<span
onClick={(e) => {
handleClick(e, index);
}}
className={`${active === index ? "active-work" : ""} work__item`}
key={index}
>
I have an outsideAlerter component that functions elsewhere on my site. I am now using it on a repeatable component and for some reason it is clearing my state effectively breaking my desired outcome.
below is my wrapper component that detects if you click outside of its children
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref, onClickOutside) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
//console.log(onClickOutside);
onClickOutside();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, props.onClickOutside);
return <div ref={wrapperRef}>{props.children}</div>;
}
Below is my controller component, it handles state
const TableFilterDropdownController = ({style, rows, colKey, activeFilters, addActiveFilter}) => {
const [tableFilterState, setTableFilterState] = useState(
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [...rows],
localActiveFilters: []
}
);
useEffect(() => {
let state = tableFilterState.state;
let localActiveFilters = tableFilterState.localActiveFilters;
if (state === INACTIVE && localActiveFilters.length > 0) {
setTableFilterState({...tableFilterState, state: ACTIVE})
}
}, [tableFilterState.state])
//filter out repeats and rows that don't match input
useEffect(() => {
let filter = tableFilterState.filter
if (filter !== "") {
let tempFilteredRows = [];
rows.map(row => {
if (row[colKey].toLowerCase().includes(filter.toLowerCase()) &&
!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows})
}
else {
let tempFilteredRows = [];
rows.map(row => {
if (!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows});
}
}, [tableFilterState.filter, rows])
const onClick = () => {
if (tableFilterState.state === DROP_DOWN) {
console.log(tableFilterState)
if (tableFilterState.localActiveFilters.length > 0) {
//setState(ACTIVE)
setTableFilterState({...tableFilterState, state: ACTIVE});
}
else {
//setState(INACTIVE)
setTableFilterState({...tableFilterState, state: INACTIVE});
}
}
else {
//setState(DROP_DOWN)
setTableFilterState({...tableFilterState, state: DROP_DOWN});
}
}
//something here is breaking it and resetting on click outside
const onClickOutside = () => {
setTableFilterState({...tableFilterState, state: INACTIVE});
}
let addLocalActiveFilter = (filter) => {
let newActiveFilters = [...tableFilterState.localActiveFilters];
const index = newActiveFilters.indexOf(filter);
if (index > -1) {
newActiveFilters.splice(index, 1);
} else {
newActiveFilters.push(filter);
}
setTableFilterState({...tableFilterState, localActiveFilters: newActiveFilters});
}
return (
<TableFilterDropdown
style={style}
color={tableFilterState.iconColor}
state={tableFilterState.state}
onClick={onClick}
onClickOutside={onClickOutside}
dropLeft={true}
filter={tableFilterState.filter}
setFilter={e => setTableFilterState({...tableFilterState, filter: e.target.value})}
>
{tableFilterState.filteredRows.map((item, index) => {
return (
<CheckboxInput
value={item}
label={item}
key={index}
onChange={e => {
addActiveFilter(e.target.value);
addLocalActiveFilter(e.target.value)
}}
isChecked={tableFilterState.localActiveFilters.includes(item)}
/>
);
})}
</TableFilterDropdown>
);
}
export default TableFilterDropdownController;
And lastly below is the UI component
const TableFilterDropdown = ({style, state, color, children, onClick, onClickOutside, dropLeft, filter, setFilter}) => {
useEffect(() => {
console.log("state change")
console.log(state);
}, [state])
return (
<div
className={`sm:relative inline-block ${style}`}
>
<OutsideAlerter onClickOutside={onClickOutside}>
<IconButton
type="button"
style={`relative text-2xl`}
onClick={onClick}
>
<IconContext.Provider value={{color: color}}>
<div>
{state === DROP_DOWN ?
<AiFillCloseCircle /> :
state === ACTIVE ?
<AiFillFilter /> :
<AiOutlineFilter />
}
</div>
</IconContext.Provider>
</IconButton>
{state === DROP_DOWN ?
<div className={`flex flex-col left-0 w-screen sm:w-32 max-h-40 overflow-auto ${dropLeft ? "sm:origin-top-left sm:left-[-2.5rem]" : "sm:origin-top-right sm:right-0"} absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10`} role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
<SearchBar label={"Search"} placeholder={"Search"} value={filter} onChange={setFilter} />
{children}
</div>
: null}
</OutsideAlerter>
</div>
);
For some reason whenever you click outside the component the tableFilterState gets set to
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [],
localActiveFilters: []
}
Which is not intentional, the tableFilterState should stay the same, only state should change. I can't figure this out so please help!!!
When you call useOutsideAlerter and pass onClickOutside handler it captures tableFilterState value and use it in a subsequent calls. This is a stale state. You could try this approach or use refs as described in docs:
const onClickOutside = () => {
setTableFilterState(tableFilterState => ({
...tableFilterState,
state: INACTIVE,
}));
}
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} //....
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.
I pulled data from an API and mapped it but I am wanting to reverse the order when the user clicks MARKET CAP. I want the user to click the
<div>MARKET CAP</div>
Down in the code I am wanting to replace this array I am mapping:
{props.coins.filter(searchingFor(search)).map...
with the reversed one I made:
const [reverseCoin, setReverseCoin] = useState(
[...coin].filter(searchingFor(search)).reverse()
);
I have no idea how to replace the original data with the reversed one so any suggestions would be great. I'm using React, styled components and hooks. Here's my code:
import Button from "../../UI/Forms/Button/Button";
import styled from "styled-components";
import Icon from "../../assets/images/sort-solid.svg";
import SearchIcon from "../../assets/images/search-solid.svg";
import * as Styles from "../../components/Table/Tables.styles";
import { Link } from "react-router-dom";
import "./PriceList.scss";
function searchingFor(search) {
return function(x) {
return x.name.toLowerCase().includes(search.toLowerCase()) || false;
};
}
//MY FUCTIONAL COMPONENT*************
//one prop has been passed to this which I called it "coins"
const PriceList = props => {
console.log(props.coins);
const [coin, setCoin] = useState([]);
const [color, setColor] = useState("");
const [MarketCapLow, setMarketCapLow] = useState(false);
const [search, setSearch] = useState("");
/// this is the variable that holds the reversed array
const [reverseCoin, setReverseCoin] = useState(
[...coin].filter(searchingFor(search)).reverse()
);
const timeIntervels = ["1H", "24H", "1W", "1M", "1Y"];
useEffect(() => {
setCoin(props.coins);
}, [props.coins]);
const updateSearch = e => {
setSearch(e.target.value);
};
const handleClick = name => {
setColor(name);
};
//creating a table for my data********
return (
<TableContainer>
<SearchBarMainContainer>
<SearchBarContainer>
<SearchInputContainer>
<img
src={SearchIcon}
width="20"
height="20"
style={{ marginRight: "16px" }}
/>
<SearchBarInput
type="text"
value={search}
onChange={updateSearch}
placeholder="Search coins..."
/>
</SearchInputContainer>
<SearchPriceChange>
{timeIntervels.map(d => (
<SearchPriceChangeItems
id={d}
onClick={() => {
handleClick(d);
}}
className={color === d ? "purple" : "black"}
>
{d}
</SearchPriceChangeItems>
))}
</SearchPriceChange>
</SearchBarContainer>
</SearchBarMainContainer>
<Styles.Tablestyles>
<tbody>
<Styles.TableRowStyles bg>
<Styles.TabelHeadingStyles bg>#</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>NAME</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>PRICE</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>CHANGE</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>
<Styles.MarketCap
onClick={() => {
setMarketCapLow(!MarketCapLow);
}}
>
<div>MARKET CAP</div>
<CoinIcon width height src={Icon} />
</Styles.MarketCap>
</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>TRADE</Styles.TabelHeadingStyles>
</Styles.TableRowStyles>
{props.coins.filter(searchingFor(search)).map(coin => {
const {
rank,
logo_url,
name,
["1d"]: { price_change_pct },
currency,
price,
market_cap
} = coin;
const newMarketPct = (price_change_pct * 100).toFixed(2);
const newPrice = Math.floor(price * 100) / 100;
const newMarketCap =
Math.abs(market_cap) > 999999999
? Math.sign(market_cap) *
(Math.abs(market_cap) / 1000000000).toFixed(1) +
"B"
: Math.sign(market_cap) * Math.abs(market_cap);
return (
<Styles.TableRowStyles key={rank}>
<Styles.TabelDataStyles>{rank}</Styles.TabelDataStyles>
<Styles.TabelDataStyles grey flex>
<CoinIcon style={{ marginRight: "12px" }} src={logo_url} />
{name} ({currency})
</Styles.TabelDataStyles>
<Styles.TabelDataStyles>${newPrice}</Styles.TabelDataStyles>
<Styles.TabelDataStyles
style={
price_change_pct.charAt(0) === "-"
? { color: "#ff2734" }
: { color: "#23cc9a" }
}
>
{newMarketPct}%
</Styles.TabelDataStyles>
<Styles.TabelDataStyles>${newMarketCap}</Styles.TabelDataStyles>
<Styles.TabelDataStyles>
<Link to={`/prices/${coin.currency}`}>
<Button padding style={{ width: "60%" }}>
Trade
</Button>
</Link>
</Styles.TabelDataStyles>
</Styles.TableRowStyles>
);
})}
</tbody>
</Styles.Tablestyles>
</TableContainer>
);
}
I'd recommend just using a flag stored in your state to toggle how the array is displayed. This allows you to avoid storing both arrays in your state when they're essentially the same data. You'd of course need to make a click handler to change the value of the reversed flag and place it wherever you want the click to occur, but I don't see where that is in your code.
[reversed, setReversed] = useState(false);
...
// declare this variable somewhere inside your component outside the return statement.
// You have create a copy to prevent reverse() from mutating the prop in place.
const displayedCoins = reversed ? Array.from(props.coins).reverse() : props.coins;
...
{displayedCoins.filter(searchingFor(search)).map((coin) => { ... });