I am currently working on a blog website and I want to list out the blogs in ascending order based on the amount of likes each blog has. However, whenever I run my current code, whenever I click “view” for one post, all of the post expand when I only want one of the post to expand, furthermore, whenever I hit “like” a like is left on all posts rather than just the one I left i like on.
Here is my code in App.js:
import { useState, useEffect } from 'react' import Blog from './components/Blog' import blogService from './services/blogs' import loginService from './services/login' import BlogForm from './components/BlogForm' import LoginForm from './components/LoginForm' import Togglable from './components/Toggable' const App = () => { const [blogs, setBlogs] = useState([]) const [user, setUser] = useState('') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [errorMessage, setErrorMessage] = useState('') useEffect(() => { blogService.getAll().then(blogs => setBlogs( blogs ) ) }, [errorMessage]) useEffect(() => { const loggedUserJSON = window.localStorage.getItem('user') if (loggedUserJSON) { const user = JSON.parse(loggedUserJSON) setUser(user) blogService.setToken(user.token) } }, []) const handleLogin = async (e) => { e.preventDefault(); try { const user = await loginService.login({ username, password }) window.localStorage.setItem('user', JSON.stringify(user)) blogService.setToken(user.token) setUser(user) setUsername('') setPassword('') } catch (exception) { setErrorMessage('Wrong credentials') setTimeout(() => { setErrorMessage(null) }, 5000) } } const handleLogout = (e) => { e.preventDefault(); window.localStorage.clear() setUser('') } if (user === '') { return ( <Togglable buttonLabel='login'> <LoginForm username={username} password={password} handleUsernameChange={({ target }) => setUsername(target.value)} handlePasswordChange={({ target }) => setPassword(target.value)} handleSubmit={handleLogin} errorMessage={errorMessage} /> </Togglable> )} else { return ( <div> <h2>blogs</h2> <h3>{errorMessage}</h3> <nobr>{user.name} logged in</nobr> <button onClick={handleLogout}>logout</button> <h2>create new</h2> <Togglable buttonLabel='create new'> <BlogForm errorMessage={errorMessage} setErrorMessage={setErrorMessage} /> </Togglable> <Blog blogs={blogs} setErrorMessage={setErrorMessage}/> </div> ) } } export default App
Finally, here is my code that deals specifically with each blog:
import { useEffect, useState, useRef } from "react" import blogService from '../services/blogs' const baseUrl = '/api/blogs' const Blog = ({blogs, setErrorMessage}) => { const [checker, setChecker] = useState(false) const [blogLikes, setBlogLikes] = useState(0) const buttonText = checker ? 'hide' : 'view' const blogStyle = { paddingTop: 10, paddingLeft: 2, border: 'solid', borderWidth: 1, marginBottom: 5 } return ( <> {blogs.map(blog => { return ( <> {buttonText === "view" ? <div style={blogStyle}> {blog.title} {blog.author} <button onClick={() => setChecker(!checker)}>{buttonText}</button> </div> : <div style={blogStyle}> {blog?.title} {blog.author} <button onClick={() => setChecker(!checker)}>{buttonText}</button> <p>{blog.url}</p> likes {blogLikes} <button onClick={(e, blog) => { e.preventDefault() setBlogLikes(blogLikes + 1) blogService.update(blog?.id, { user: blog.user?.id, likes: blogLikes, author: blog.author, title: blog.title, url: blog.url}) setErrorMessage(`You liked ${blog.author}s post`) }}>like</button> <p>{blog.user?.username}</p> </div> }</>)})} </> ) } export default Blog
Advertisement
Answer
The mistake you did here is you created Blog.js
(or whatever name you gave to the source file), you are treating it as an individual blog, and expecting ‘blogLikes` to work for an individual blog.
Create separate components for list and item and include list component into the app. The quick fix will be,
App.js
import { useState, useEffect } from 'react' import BlogList from './components/Blog' import blogService from './services/blogs' import loginService from './services/login' import BlogForm from './components/BlogForm' import LoginForm from './components/LoginForm' import Togglable from './components/Toggable' const App = () => { const [blogs, setBlogs] = useState([]) const [user, setUser] = useState('') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [errorMessage, setErrorMessage] = useState('') useEffect(() => { blogService.getAll().then(blogs => setBlogs( blogs ) ) }, [errorMessage]) useEffect(() => { const loggedUserJSON = window.localStorage.getItem('user') if (loggedUserJSON) { const user = JSON.parse(loggedUserJSON) setUser(user) blogService.setToken(user.token) } }, []) const handleLogin = async (e) => { e.preventDefault(); try { const user = await loginService.login({ username, password }) window.localStorage.setItem('user', JSON.stringify(user)) blogService.setToken(user.token) setUser(user) setUsername('') setPassword('') } catch (exception) { setErrorMessage('Wrong credentials') setTimeout(() => { setErrorMessage(null) }, 5000) } } const handleLogout = (e) => { e.preventDefault(); window.localStorage.clear() setUser('') } if (user === '') { return ( <Togglable buttonLabel='login'> <LoginForm username={username} password={password} handleUsernameChange={({ target }) => setUsername(target.value)} handlePasswordChange={({ target }) => setPassword(target.value)} handleSubmit={handleLogin} errorMessage={errorMessage} /> </Togglable> )} else { return ( <div> <h2>blogs</h2> <h3>{errorMessage}</h3> <nobr>{user.name} logged in</nobr> <button onClick={handleLogout}>logout</button> <h2>create new</h2> <Togglable buttonLabel='create new'> <BlogForm errorMessage={errorMessage} setErrorMessage={setErrorMessage} /> </Togglable> <BlogList blogs={blogs} setErrorMessage={setErrorMessage}/> </div> ) } } export default App
BlogList.js
To sort blogs by likes you apply Array.sort()
before mapping.
import Blog from './Blog'; const BlogList = ({blogs, setErrorMessage}) => { return ( <> { blogs.sort((a, b)=> a.likes > b.likes ? 1 : -1 ).map((blog, i) => <Blog key={i} blog={blog} setErrorMessage={setErrorMessage} /> } </> ) } export default BlogList;
Blog.js
import blogService from '../services/blogs'; import { useEffect, useState, useRef } from 'react'; const Blog = ({blog, setErrorMessage}) => { const [checker, setChecker] = useState(false) const [blogLikes, setBlogLikes] = useState(blog.likes) const buttonText = checker ? 'hide' : 'view' const blogStyle = { paddingTop: 10, paddingLeft: 2, border: 'solid', borderWidth: 1, marginBottom: 5 } return ( <> {buttonText === "view" ? <div style={blogStyle}> {blog.title} {blog.author} <button onClick={() => setChecker(!checker)}>{buttonText}</button> </div> : <div style={blogStyle}> {blog?.title} {blog.author} <button onClick={() => setChecker(!checker)}>{buttonText}</button> <p>{blog.url}</p> likes {blogLikes} <button onClick={(e, blog) => { e.preventDefault() setBlogLikes(blogLikes + 1) blogService.update(blog?.id, { user: blog.user?.id, likes: blogLikes, author: blog.author, title: blog.title, url: blog.url}) setErrorMessage(`You liked ${blog.author}s post`) }}>like</button> <p>{blog.user?.username}</p> </div> } </> ) } export default Blog
Since you have a separate state for likes here in the Blog component its action or data won’t affect other blog components.
[update]
If you are familiar with context
then try it, for the beginner you move like action to App.js
and pass it as a prop just like you did for setErrorMessage
. Also, add empty useEffect
with blogs
dependency in App.js
so its change will cause re-render.