Should I use the Redux store to pass data from child to parent? - javascript

I am building a to-do/notes app in order to learn the basics of Redux, using React hooks and Typescript.
A note is composed of an ID and a value. The user can add, delete or edit a note.
The add / delete mechanics work fine. But the edit one is trickier for me, as I'm questionning how it should be implemented.
I think my reducer's code is fine. The problem lies between my component (Note.tsx) and its parent one (App.tsx).
When i'm logging the value, I can see that the new updated/edited value of the note is not sent to the reducer. As a result, my note is not edited with the new value.
I've tried "cloning" the redux store and making my changes here, but it seems tedious and unnatural to me. Should I just call the edit method from my Note.tsx component ?
Is there a clean / conventional way to do this ?
Here is my code :
App.tsx
function App() {
const notes = useSelector<NotesStates, NotesStates['notes']>(((state) => state.notes));
const dispatch = useDispatch();
const onAddNote = (note: string) => {
dispatch(addNote(note));
};
const onDeleteNote = (note: NoteType) => {
dispatch(deleteNote(note));
};
const onEditNote = (note: NoteType) => {
dispatch(updateNote(note));
};
return (
<div className="home">
<NewNoteInput addNote={onAddNote} />
<hr />
<ul className="notes">
{notes.map((note) => (
<Note
updateNote={() => onEditNote(note)}
deleteNote={() => onDeleteNote(note)}
note={note}
/>
))}
</ul>
</div>
);
}
Note.tsx
interface NoteProps {
deleteNote(): void
updateNote(noteValue: string | number): void
note: NoteType
}
const Note: React.FC<NoteProps> = ({ deleteNote, updateNote, note: { id, value } }) => {
const [isEditing, setIsEditing] = useState(false);
const [newNoteValue, setNewNoteValue] = useState(value);
const onDeleteNote = () => {
deleteNote();
};
const onUpdateNote = () => {
updateNote(newNoteValue);
setIsEditing(false);
};
const handleOnDoubleClick = () => {
setIsEditing(true);
};
const renderBody = () => {
if (!isEditing) {
return (
<>
{!value && <span className="empty-text">Note is empty</span>}
<span>{value}</span>
</>
);
}
return (
<input
value={newNoteValue}
onChange={(e) => setNewNoteValue(e.target.value)}
onBlur={onUpdateNote}
/>
);
};
return (
<li className="note" key={id}>
<span className="note__title">
Note n°
{id}
</span>
<div className="note__body" onDoubleClick={handleOnDoubleClick}>
{renderBody()}
</div>
<button type="button" onClick={onDeleteNote}>Delete</button>
</li>
);
};
export default Note;
and the notesReducer.tsx
export interface NotesStates {
notes: Note[]
}
export interface Note {
id: number
value: string
}
const initialState = {
notes: [],
};
let noteID = 0;
export const notesReducer = (state: NotesStates = initialState, action: NoteAction): NotesStates => {
switch (action.type) {
case 'ADD_NOTE': {
noteID += 1;
return {
...state,
notes: [...state.notes, {
id: noteID,
value: action.payload,
}],
};
}
case 'UPDATE_NOTE': {
return {
...state,
notes: state.notes.map((note) => {
if (note.id === action.payload.id) {
return {
...note,
value: action.payload.value,
};
}
return note;
}),
};
}
case 'DELETE_NOTE': {
return {
...state,
notes: [...state.notes
.filter((note) => note.id !== action.payload.id)],
};
}
default:
return state;
}
};

Thanks to #secan in the comments I made this work, plus some changes.
In App.tsx :
<Note
updateNote={onEditNote}
deleteNote={() => onDeleteNote(note)}
note={note}
/>
In Note.tsx :
interface NoteProps {
deleteNote(): void
updateNote(newNote: NoteType): void // updated the signature
note: NoteType
}
// Now passing entire object instead of just the value
const onUpdateNote = (newNote: NoteType) => {
updateNote(newNote);
setIsEditing(false);
};
const renderBody = () => {
if (!isEditing) {
return (
<>
{!value && <span className="empty-text">Note is empty</span>}
<span>{value}</span>
</>
);
}
return (
<input
value={newNoteValue}
onChange={(e) => setNewNoteValue(e.target.value)}
// modifying current note with updated value
onBlur={() => onUpdateNote({ id, value: newNoteValue })}
/>
);
};

Related

can´t access specific element of my array React

I've a problem with my react app.The navbar worked correctly before the ItemDetailContainer, and i want to show in the DOM an specific product. When adding the ItemDetailContainer to app.js, the page doesn't show anything (including the navbar) . Here's my code:
ItemDetailContainer.jsx:
const {products} = require('../utils/data');
const ItemDetailContainer = () => {
const [arrayList, SetArrayList] = useState({});
useEffect(() => {
customFetch(2000, products[0])
.then(result => SetArrayList(result))
.catch(err => console.log(err))
}, [])
return (
<div>
<ItemDetail products={arrayList}/>
</div>
);
}
here's my array in data.js that i want to access:
const products = [
{
id: 1,
image: "https://baltimore.com.ar/img/articulos/4188.png",
title: "Vino Abras Malbec 750cc",
price: 2.500,
stock: 8,
initial: 1
}
ItemDetail.jsx:
const ItemDetail = ({product}) => {
return(
<>
{product.image}
{product.title}
</>
)
}
CustomFetch.js:
let is_ok = true
let customFetch = (time, array) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (is_ok) {
resolve(array)
} else {
reject("error")
}
}, time)
})
}
and my app.js:
function App() {
return (
<div>
<Navbar/>
<ItemDetailContainer/>
</div>
);
}
I've done all the imports and exports, but I dont' know what's the problem.
Since you are passing a product prop in <ItemDetail product={arrayList}/> within ItemDetailContainer make sure you destructure your props correctly in ItemDetail component,like so :
const ItemDetail = ({product}) => {
return(
<>
{product.image}
{product.title}
</>
)
}
Which is basically like this :
const ItemDetail = (props) => {
return (
<>
{props.product.image}
{props.product.title}
</>
);
};
check it out in code sandbox

Why is my setter function provided by the context api not working in the handle function

I have this implementation for a search filter using tags.
https://codesandbox.io/s/search-filter-tags-zgfcs?file=/src/Tags.tsx:622-626
I for the life of me cannot figure out why my setTags() method is not working in the handleChange function, but is working fine in the handleAllChange function.
Tags.tsx
import * as React from "react";
import { TagContext } from "./TagContext";
let AllTags = [
"Html",
"CSS",
"JavaScript",
"ReactJS",
"GitHub",
"TypeScript",
"Celebal"
];
const Tags = () => {
const { tags, setTags } = React.useContext(TagContext);
const [allChecked, setAllChecked] = React.useState(false);
// const [tags, setTags] = React.useState([] as string[]);
const handleAllChange = () => {
if (!allChecked) {
setTags(AllTags);
} else {
setTags([]);
}
setAllChecked(!allChecked);
};
const handleChange = (name: string) => {
let temp = tags;
if (temp.includes(name)) {
temp.splice(temp.indexOf(name), 1);
} else {
temp.push(name);
}
console.log("temp - ", temp);
setTags(temp);
};
React.useEffect(() => {
console.log("Tags - ", tags);
}, [tags]);
return (
<>
<h1>Filter Tags</h1>
<div className="tags-container">
<div className="w-fit p-2 m-2">
<button
className="p-1 border-2 border-black rounded font-semibold"
onClick={handleAllChange}
>
Apply All
</button>
</div>
{AllTags.map((tag, i) => {
return (
<div className="w-fit p-2 m-2" key={i}>
<input
className="mt-2"
type="checkbox"
value={tag}
name={tag}
onChange={(e) => handleChange(tag)}
checked={tags.includes(tag)}
/>
<label htmlFor={tag}>#{tag}</label>
</div>
);
})}
</div>
</>
);
};
export default Tags;
The tags, and setTags are coming from context API.
TagContext.tsx
import * as React from "react";
interface ITagContext {
tags: any;
setTags: any;
handleChange: (name: string) => void;
}
export const TagContext = React.createContext({} as ITagContext);
interface ITagProvider {
children: React.ReactNode;
}
export const TagProvider = ({ children }: ITagProvider) => {
const [tags, setTags] = React.useState([] as string[]);
const handleChange = (name: string) => {
debugger;
let tag = name;
let temp = tags;
if (temp.includes(tag)) {
temp.splice(temp.indexOf(tag), 1);
} else {
temp.push(tag.toString());
}
setTags(temp);
};
React.useEffect(() => {
debugger;
console.log("Tags in Context - ", tags);
}, [tags]);
return (
<TagContext.Provider value={{ tags, setTags, handleChange }}>
{children}
</TagContext.Provider>
);
};
In your handleChange() function you're using a reference to the tags array:
let temp = tags;
That means that when you eventually call setTags(temp); the reference has not changed and your Tags component fails to re-render. Create a new array instead:
let temp = [...tags];

Edit Note in react redux

Im trying to make editing note functionality in my notes app and stuck on this point. Can somebody help me with it? In this version of code I have error 'dispatch is not a function' in EDIT_NOTE reducer.
So, here is my:
Actions:
import { ADD_NOTE, DELETE_NOTE, EDIT_NOTE } from './actionTypes'
export const editNoteAction = (text, id) => ({
type: EDIT_NOTE,
payload: {
id,
text,
},
})
Reducer:
import { nanoid } from 'nanoid'
import { ADD_NOTE, DELETE_NOTE, EDIT_NOTE } from './actionTypes'
const date = new Date()
export const initialState = {
notes: [
{
id: nanoid(),
text: '',
date: '',
},
],
}
export default function notes(state = initialState, { type, payload }) {
console.log(type)
switch (type) {
case ADD_NOTE: {
...
case DELETE_NOTE: {
...
case EDIT_NOTE: {
const editedNote = {
text: payload.text,
id: payload.id,
date: date.toLocaleDateString(),
}
return {
notes: state.notes.map((note) =>
note.id === payload.id
? {
...state,
notes: [...state.notes, editedNote],
}
: state
),
}
}
default:
return state
}
}
NoteList - the component where all notes are collected:
const NoteList = ({
Notes,
addNoteAction,
deleteNoteAction,
editNoteAction,
}) => {
console.log(Notes)
return (
<div className={styles.notesList}>
<AddNote handleAddNote={addNoteAction} />
{Notes.map((note) => (
<Note
key={note.id}
id={note.id}
text={note.text}
date={note.date}
handleDeleteNote={deleteNoteAction}
handleEditNote={editNoteAction}
/>
))}
</div>
)
}
const mapStateToProps = (state) => ({
Notes: state.notes || [],
})
const mapDispatchToProps = (dispatch) => ({
addNoteAction: (text) => dispatch(addNoteAction(text)),
deleteNoteAction: (id) => dispatch(deleteNoteAction(id)),
editNoteAction: (id) => dispatch(editNoteAction(id)),
})
export default connect(mapStateToProps, mapDispatchToProps)(NoteList)
And Note component
const Note = ({
id,
text,
date,
handleDeleteNote,
handleEditNote,
editNoteAction,
Notes,
}) => {
const [animType, setAnimType] = useState(AnimationTypes.ANIM_TYPE_ADD)
const [isEdit, setIsEdit] = useState(false)
const handleToggleEdit = () => {
if (!isEdit) {
editNoteAction(text)
}
setIsEdit(!isEdit)
}
const [newText, setNewText] = useState('')
const handleTextChange = (e) => {
setNewText(e.target.value)
}
const onNoteSave = () => {
handleToggleEdit()
editNoteAction({
id,
date,
text: newText,
})
}
return (
<div className={classnames(styles.note, styles[animType])}>
{isEdit ? (
<IsEditingNote
formalClassName={styles.editNote}
formalRef={noteTextareaRef}
formalOnChange={handleTextChange}
formalValue={newText}
/>
) : (
<span className={styles.noteText}>{text}</span>
)}
<div className={styles.noteFooter}>
<small>{date}</small>
<div className={styles.footerIcons}>
{!isEdit ? (
<EditingIcon
formalClassName={styles.editIcon}
formalOnClick={handleToggleEdit}
/>
) : (
<MdCheck
className={styles.deleteIcon}
size="1.4em"
onClick={onNoteSave}
/>
)}
<MdDeleteForever
className={styles.deleteIcon}
size="1.2em"
onClick={() => {
setAnimType(AnimationTypes.ANIM_TYPE_DELETE)
setTimeout(() => handleDeleteNote(id), 500)
}}
/>
</div>
</div>
</div>
)
}
const mapStateToProps = (state) => ({
Notes: state.notes || [],
})
const mapDispatchToProps = (dispatch) => ({
editNoteAction: (editedNote) => dispatch(editNoteAction(editedNote)),
})
export default connect(mapDispatchToProps, mapStateToProps)(Note)

How to modify a specific component of a list of component rendered using map in react?

I have a PostList component with an array of posts objects. I am rendering this list of post using another pure functional component Post using Array.map() method. Post component has another component - LikeButton to like or unlike a post. Now I want to show a spinner during like or unlike on top of that LikeButton component. LikeButton Component looks something like this:
const LikeButton = (props) => {
const likeBtnClasses = [classes.LikeBtn];
const loggedInUserId = useSelector((state) => state.auth.user.id);
const isLoading = useSelector((state) => state.post.loading);
const isPostLiked = props.post.likes.find(
(like) => like.user === loggedInUserId
);
const [isLiked, setLike] = useState(isPostLiked ? true : false);
const token = useSelector((state) => state.auth.token);
const dispatch = useDispatch();
if (isLiked) {
likeBtnClasses.push(classes.Highlight);
}
const postLikeHandler = () => {
if (!isLiked) {
setLike(true);
dispatch(actions.likePost(props.post._id, token));
} else {
setLike(false);
dispatch(actions.unlikePost(props.post._id, token));
}
};
return isLoading ? (
<Spinner />
) : (
<button
className={likeBtnClasses.join(" ")}
onClick={() => postLikeHandler()}
>
<i class="far fa-thumbs-up"></i>
<small>{props.post.likes.length}</small>
</button>
);
};
Instead of showing the spinner to that single post, I am seeing it on all the posts.
My Post component looks like this:
const Post = (props) => {
return (
<div className={classes.Post}>
<div className={classes.Author}>
<img src={props.postData.avatar} alt="avatar" />
<div className={classes.AuthorDetails}>
<h3>{props.postData.name}</h3>
</div>
</div>
<div className={classes.PostText}>
<p>{props.postData.text}</p>
</div>
<hr />
<div className={classes.PostTools}>
<LikeButton post={props.postData} />
<div className={classes.PostBtn}>
<i class="far fa-comments"></i>
<small>3</small>
</div>
<div className={classes.PostBtn}>
<i class="fas fa-share"></i>
<small>2</small>
</div>
</div>
</div>
);
};
PostList component:
class PostList extends React.Component {
state = {
posts: [
{
text: "POST1",
user: "XYZ",
name: "XYZ",
id: "post1",
likes: [],
},
{
text: "POST2",
user: "johndoe#test.com",
name: "John Doe",
id: "post2",
likes: [],
},
],
};
componentDidMount() {
if (this.props.token) {
this.props.onFetchPosts(this.props.token);
this.props.onFetchUserAuthData(this.props.token);
}
}
render() {
let posts = null;
if (this.props.posts.length === 0) {
posts = this.state.posts.map((post) => {
return <Post key={post.id} postData={post} />;
});
} else {
posts = this.props.posts.map((post) => {
return <Post key={post._id} postData={post} />;
});
}
return (
<div>
<CreatePost />
{posts}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
token: state.auth.token,
posts: state.post.posts,
loading: state.post.loading,
error: state.post.err,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onFetchPosts: (token) => dispatch(actions.fetchPosts(token)),
onFetchUserAuthData: (token) => dispatch(actions.fetchUser(token)),
};
};
Please do some change in your to checking like/unlike is loading or not for the LikeButton.
const LikeButton = (props) => {
....
const [isButtonLoading, setButtonLoading] = useState(false);
...
return isButtonLoading ? (
<Spinner />
) : (
<button
className={likeBtnClasses.join(" ")}
onClick={() => postLikeHandler();setButtonLoading(true)}
>
<i class="far fa-thumbs-up"></i>
<small>{props.post.likes.length}</small>
</button>
);
};
Then on your dispatch callback need to set the isButtonLoading value to false.
const buttonCallback() {
// here we need to reset our flag
setButtonLoading(false);
}
const postLikeHandler = () => {
if (!isLiked) {
setLike(true);
// for this action you need to create third parameter called as callback so after response our buttonCallback will call
dispatch(actions.likePost(props.post._id, token, buttonCallback));
} else {
setLike(false);
// for this action you need to create third parameter called as callback so after response our buttonCallback will call
dispatch(actions.unlikePost(props.post._id, token, buttonCallback);
}
};
fore more details please check here.
Hope this will help you.

Why is toggle todo not working in Redux?

So, I have been working through Dan Abramov's Redux tutorial where you build a simple todo application. Here is the code for the main render function,
const todo = (state, action) => {
switch(action.type){
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
}
case 'TOGGLE_TODO':
if(state.id !== action.id){
return state
}
return {...state,
completed: !state.completed
}
default:
return state
}
}
const todos = (state = [], action) => {
switch(action.type){
case "ADD_TODO":
return [
...state,
todo(undefined, action)
]
case "TOGGLE_TODO":
return state.map(t => {
todo(t, action)
})
default:
return state;
}
}
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch(action.type){
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
});
const store = createStore(todoApp);
const FilterLink = ({
filter,
currentFilter,
children
}) => {
if(filter === currentFilter){
return <span>{children}</span>
}
return (
<a href='#' onClick={e => {
e.preventDefault();
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter
})
}}>
{children}
</a>
)
}
const Todo = ({
onClick,
completed,
text
}) => (
<li onClick={(onClick)}
style={{textDecoration: completed ? 'line-through' : 'none'}}>
{text}
</li>
);
const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo key={todo.id} {...todo}
onClick={() => onTodoClick(todo.id)} />
)}
</ul>
);
const getVisibleTodos = (todos, filter) => {
switch(filter){
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
)
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
)
}
}
let nextTodoId = 0;
class TodoApp extends React.Component {
render() {
const {
todos,
visibilityFilter
} = this.props
const visibleTodos = getVisibleTodos(todos, visibilityFilter);
return (
<div>
<input ref={text => {
this.input = text;
}} />
<button onClick={() => {
store.dispatch({
type:"ADD_TODO",
text: this.input.value,
id: nextTodoId++
});
this.input.value = '';
}}>Add a todo
</button>
<TodoList todos={visibleTodos} onTodoClick={id => store.dispatch({
type: 'TOGGLE_TODO',
id
})} />
<p>
Show:
{' '}
<FilterLink filter='SHOW_ALL' currentFilter={visibilityFilter}>
All
</FilterLink>
{' '}
<FilterLink filter='SHOW_COMPLETED' currentFilter={visibilityFilter}>
Completed
</FilterLink>
{' '}
<FilterLink filter='SHOW_ACTIVE' currentFilter={visibilityFilter}>
Active
</FilterLink>
</p>
</div>
)
}
}
const render = () => {
console.log(store.getState());
ReactDOM.render(<TodoApp {...store.getState()}/>, document.getElementById('root'));
}
store.subscribe(render);
render();
When I try to toggle the todo, I get the following error,
index.js:170 Uncaught TypeError: Cannot read property 'id' of undefined
Now, when I tried logging out this.props.todos on the event of my trying to toggle a todo, it returns undefined. This is the reason why I get the error, because for some reason this.props.todos is not being passed on the click event. However, I went through the course notes and I have the exact same code. What am I doing wrong here? And how do I fix it?
Is this the example you are following? https://github.com/reactjs/redux/tree/master/examples/todos
Not sure if it is down to this, but in your Todo you might want to remove the () around onClick.
const Todo = ({
onClick,
completed,
text
}) => (
<li onClick={ onClick } /* <- this bit */
style={{textDecoration: completed ? 'line-through' : 'none'}}>
{text}
</li>
);
The problem is in your todos reducer for the "TOGGLE_TODO" case. You have this code:
return state.map(t => {todo(t, action)})
The brackets are unnecessary and cause the arrow function to expect a return statement. Since there isn't a return statement, the return value is undefined, so you get an array of undefined values.
Change it to
return state.map(t => todo(t, action));

Categories

Resources