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
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
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];
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)
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.
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));