I'm learning redux hooks from library "react-redux" because I need to apply Redux also in the functional components of my project.
So far I don't understand how can be used the same project structure of the redux HOC with connect that I use for the class components.
Specifically I have a separate action file which invoke my API with axios:
FoosActions.js
import axios from "axios";
import {
GET_FOO,
} from "./Types";
};
export const getFoo = () => async (dispatch) => {
const res = await axios.get("/api/v1/foos");
dispatch({
type: GET_FOO,
payload: res.data,
});
};
FooList.js:
import { connect } from "react-redux";
import { getFoos } from "../../actions/FoosActions";
class FoosList extends Component {
constructor() {
super();
this.state = {
errors: {},
};
}
componentDidMount() {
this.props.getFoos();
}
render() {
const { data } = this.props.foo;
return (
<div className="container">
<h2>foo data fetched from API endpoint : </h2>
<ul>
{data.map((foo) => {
return (
<li>
{foo.id} - {foo.name}
</li>
);
})}
<ul>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({
foo: state.foo,
errors: state.errors,
});
export default connect(mapStateToProps, { getFoos })(FooList);
FooReducer,js
import { GET_FOO} from "../actions/Types";
const initialState = {
foos: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case GET_FOO:
return {
...state,
foos: action.payload,
};
Now instead in my Functional Component:
FooListFC.js
import { useDispatch, useSelector } from "react-redux";
import { getFoo } from "../../actions/FoosActions";
const Mapping = (props) => {
const [foo, setFoo] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getFoo());
const fooRetrieved = useSelector((state) => state.foo);
setFoo(fooRetrieved);
}, []);
return (
<div className="container">
<h2>foo data fetched from API endpoint : </h2>
<ul>
{foo.map((foo) => {
return (
<li>
{foo.id} - {foo.name}
</li>
);
})}
</ul>
</div>
)
}
How can I reproduce the same behavior of fetching data from API in class component with actions in a different file and using redux hooks (my code in the functional component is not working) ?
Is it a bad practice having both approaches in the same project?
you are able to reproduce the same behaviour, in the function component you can use the selector only instead of both useSelector and useState:
const Mapping = (props) => {
const foo = useSelector((state) => state.foo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getFoo());
}, []);
...
Related
Hi I have 2 components.
The first component provides a read (useSelector) from the Redux state object and renders its contents
The second component ensures the insertion of new data into the same Redux state object
How to achieve that when a Redux state object changes with the help of the second component, the first component captures this change and renders the new content of the object again.
I tried to add in the component element:
useEffect(() => {
...some actions
}, [reduxStateObject]);
But it gives me too many requests.
/// EDIT add real example
component
import React from "react";
import { useSelector } from "react-redux";
const ToDoList = () => {
const { todos } = useSelector((state) => state.global);
return (
<div>
<h1>Active</h1>
{todos
?.filter((todo) => !todo.isCompleted)
.sort((a, b) => (a.deadline < b.deadline ? 1 : -1))
.map((todo, id) => {
const date = new Date(todo.deadline).toLocaleString();
return (
<div key={id}>
<p>{todo.text}</p>
<p>{date}</p>
</div>
);
})}
</div>
);
};
export default ToDoList;
component
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getToDoItems } from "../redux/globalSlice";
import ToDoList from "../components/ToDoList";
const ToDoWall = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getToDoItems(1));
}, [dispatch]);
const submitForm = (e) => {
dispatch(postToDoItem(e.data));
};
return (
<>
<ToDoList />
<form onSubmit={submitForm}>
<input type="text"></input>
<input type="submit" value="" />
</form>
</>
);
};
export default ToDoWall;
/// EDIT add Reducer
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: null,
};
export const globalSlice = createSlice({
name: "global",
initialState,
reducers: {
setItems: (state, action) => {
state.todos = action.payload;
},
},
});
export const { setItems } = globalSlice.actions;
export default globalSlice.reducer;
// Load todo items
export const getToDoItems = (id) => {
return (dispatch) => {
axios
.get(`https://xxx.mockapi.io/api/list/${id}/todos`)
.then((resp) => dispatch(setItems(resp.data)));
};
};
// Post a list name
export const postNameList = (data) => {
return (dispatch) => {
axios.post("https://xxx.mockapi.io/api/list", {
name: data,
});
};
};
// Post a todo item
export const postToDoItem = (id, data) => {
return (dispatch) => {
axios.post(
`https://xxx.mockapi.io/api/list/${id}/todos`,
{
listId: id,
title: data.title,
text: data.text,
deadline: +new Date(data.deadline),
isCompleted: false,
}
);
};
};
As far as I understood, you don't need to do anything. When you dispatch action to change state in redux store, it'll change, and all components that use that state will get it, you don't need to worry about updating anything.
I'm trying to get a bunch of articles from API using axios and useContext hook in React, but getting 'null' as a response.
This is the code from "State" file
import React, { useReducer } from "react";
import axios from "axios";
import ArticleContext from "./articleContext";
import articleReducer from "./articleReducer";
import { GET_ARTICLE } from "../types";
const ArticleState = (props) => {
const initialState = {
article: null,
};
const [state, dispatch] = useReducer(articleReducer, initialState);
const getArticle = async (id) => {
try {
const res = await axios.get(`/articles/${id}`);
dispatch({ type: GET_ARTICLE, payload: res.data });
} catch (err) {
console.log("errrrr");
}
};
return (
<ArticleContext.Provider
value={{
article: state.article,
getArticle,
}}
>
{props.children}
</ArticleContext.Provider>
);
};
export default ArticleState;
This is code from "Reducer"
import { GET_ARTICLE } from "../types";
// eslint-disable-next-line import/no-anonymous-default-export
export default (state, action) => {
switch (action.type) {
case GET_ARTICLE:
return {
...state,
article: action.payload,
};
default:
return state;
}
};
And finally code from the component, where i' trying to render data from the api call response and getting TypeError: article is null Am i missing something here? The main App component is also wrapped in <ArticleState></ArticleState>.
import React, { useEffect, useContext } from "react";
import ArticleContext from "../../context/article/articleContext";
const Article = () => {
const articleContext = useContext(ArticleContext);
const { article, getArticle } = articleContext;
useEffect(() => {
getArticle();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="article" key={article.id}>
<h2 className="article__title">{article.Title}</h2>
<p className="article__body">{article.preview}</p>
</div>
);
};
export default Article;
You should check if the article has been set before displaying its data.
Add a condition to the component before rendering the article informations:
const Article = () => {
const articleContext = useContext(ArticleContext);
const { article, getArticle } = articleContext;
useEffect(() => {
getArticle();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!article) {
return <>Loading article...</>
}
return (
<div className="article" key={article.id}>
<h2 className="article__title">{article.Title}</h2>
<p className="article__body">{article.preview}</p>
</div>
);
};
I just can't decide the pattern I want to follow.
I'm implementing what I call a UserParent component. Basically a list of users and when you click on a user, it loads their resources.
Approach 1: Redux
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import { getUserResources, clearResources } from './userSlice'
import CircularProgress from '#mui/material/CircularProgress';
import { getAllUsers } from './userSlice';
export const UserParent = () => {
const users = useSelector((state) => state.users.users )
const resources = useSelector((state) => state.users.user.resources )
const [highLightedUsers, setHighLightedItems] = useState([]);
const isLoading = useSelector((state) => state.users.isLoading)
let dispatch = useDispatch();
useEffect(() => {
dispatch(getAllUsers());
}, [])
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
dispatch(clearResources())
} else {
setHighLightedItems([...highLightedUsers, user.label])
dispatch(getUserResources(user.id))
}
}
return(
<>
{ isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
<div className="search-container">
<div className="search-nav">
<NavList
items={users}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div> }
</>
)
}
And then we have the reducer code:
import { createSlice } from '#reduxjs/toolkit';
import Api from '../../services/api';
const INITIAL_STATE = {
users: [],
isLoading: true,
user: { resources: [] }
};
export const userSlice = createSlice({
name: 'users',
initialState: INITIAL_STATE,
reducers: {
loadAllUsers: (state, action) => ({
...state,
users: action.payload,
isLoading: false
}),
toggleUserLoader: (state, action) => ({
...state,
isLoading: action.payload
}),
loadUserResources: (state, action) => ({
...state, user: { resources: action.payload }
}),
clearResources: (state) => ({
...state,
isLoading: false,
user: { resources: [] }
})
}
});
export const {
loadAllUsers,
toggleUserLoader,
loadUserResources,
clearResources
} = userSlice.actions;
export const getAllUsers = () => async (dispatch) => {
try {
const res = await Api.fetchAllUsers()
if (!res.errors) {
dispatch(loadAllUsers(res.map(user => ({id: user.id, label: user.full_name}))));
} else {
throw res.errors
}
} catch (err) {
alert(JSON.stringify(err))
}
}
export const getUserResources = (userId) => async (dispatch) => {
try {
const res = await Api.fetchUserResources(userId)
if (!res.errors) {
dispatch(loadUserResources(res));
} else {
throw res.errors
}
} catch (err) {
alert(JSON.stringify(err))
}
}
export default userSlice.reducer;
This is fine but I am following this pattern on every page in my app. While it is easy follow I don't believe I'm using global state properly. Every page makes and API call and loads the response into redux, not necessarily because it needs to be shared (although it may be at some point) but because it's the pattern I'm following.
Approach 2: Local State
import React, { useEffect, useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import Api from '../../services/api';
export const UserParent = () => {
const [users, setUsers] = useState([])
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const getUsers = async () => {
try {
const res = await Api.fetchAllUsers()
setUsers(res.map(user => ({id: user.id, label: user.full_name})))
setIsLoading(false)
} catch (error) {
console.log(error)
}
}
const getUserResources = async (userId) => {
try {
setIsLoading(true)
const res = await Api.fetchUserResources(userId)
setResources(res)
setIsLoading(false)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getUsers()
}, [])
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.label])
getUserResources(user.id)
}
}
return(
<>
{ isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
<div className="search-container">
<div className="search-nav">
<NavList
items={users}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>}
</>
)
}
What I like about this is that it uses local state and doesn't bloat global state however, I don't like that it still has business logic in the component, I could just move these to a different file but first I wanted to try React Query instead.
Approach 3: React Query
import React, { useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import Api from '../../services/api';
import { useQuery } from "react-query";
export const UserParent = () => {
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const getUsers = async () => {
try {
const res = await Api.fetchAllUsers()
return res
} catch (error) {
console.log(error)
}
}
const { data, status } = useQuery("users", getUsers);
const getUserResources = async (userId) => {
try {
const res = await Api.fetchUserResources(userId)
setResources(res)
} catch (error) {
console.log(error)
}
}
const onUserClick = (user) => {
if (highLightedUsers.includes(user.label)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.label])
getUserResources(user.id)
}
}
return(
<>
{ status === 'loading' && <CircularProgress className="search-loader" /> }
<div className="search-container">
<div className="search-nav">
<NavList
items={data.map(user => ({id: user.id, label: user.full_name}))}
onItemClick={onUserClick}
highLightedItems={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>
</>
)
}
This is great but there is still business logic in my component, so I can move those functions to a separate file and import them and then I end up with this:
import React, { useState } from 'react'
import { UserList } from '../users/UserList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '#mui/material/CircularProgress';
import { getUsers, getUserResources } from './users'
import { useQuery } from "react-query";
export const UserParent = () => {
const [resources, setResources] = useState([])
const [highLightedUsers, setHighLightedItems] = useState([]);
const { data, status } = useQuery("users", getUsers);
const onUserClick = async (user) => {
if (highLightedUsers.includes(user.full_name)) {
setHighLightedItems([])
} else {
setHighLightedItems([...highLightedUsers, user.full_name])
const res = await getUserResources(user.id)
setResources(res)
}
}
return(
<>
{ status === 'loading' && <CircularProgress className="search-loader" /> }
<div className="search-container">
<div className="search-nav">
<UserList
users={data}
onUserClick={onUserClick}
highLightedUsers={highLightedUsers}
/>
</div>
<div className="search-results">
<ResourceList resources={resources} />
</div>
</div>
</>
)
}
In my opinion this is so clean! However, is there anything wrong with the first approach using Redux? Which approach do you prefer?
The first approach you are using shows a very outdated style of Redux.
Modern Redux is written using the official Redux Toolkit (which is the recommendation for all your production code since 2019. It does not use switch..case reducers, ACTION_TYPES, immutable reducer logic, createStore or connect. Generally, it is about 1/4 of the code.
What RTK also does is ship with RTK-Query, which is similar to React Query, but even a bit more declarative than React Query. Which one of those two you like better is probably left to personal taste.
I'd suggest that if you have any use for Redux beyond "just api fetching" (which is a solved problem given either RTK Query or React Query), you can go with Redux/RTK-Query.
If you don't have any global state left after handling api caching, you should probably just go with React Query.
As for learning modern Redux including RTK Query, please follow the official Redux tutorial.
Personally I prefer React-Query for all API-calls, it is great it useMutate and how it manages re-fetching, invalidating queries and more.
I am using your third approach where I create the queries in separate files and then import them where needed.
So far it has been great, and I am using RecoilJS for managing global states. And with the right approach there is really not much that actually needs to be in a global state IMO. Some basic auth/user info and perhaps notification management. But other than that I have been putting less and less in global states keeping it much simpler and scoped.
Hi im new to redux and im trying to create a movie app using the API from www.themoviedb.org. I am trying to display the popular movies and im sure the API link works since ive tested it in postman but i cant seem to figure out why redux doesnt pick up the data.
//action
import { FETCH_POPULAR } from "./types";
import axios from "axios";
export const fetchPopularMovies = () => (dispatch) => {
axios
.get(
`https://api.themoviedb.org/3/movie/popular?api_key=${API}&language=en-US`
)
.then((response) =>
dispatch({
type: FETCH_POPULAR,
payload: response.data
})
)
.catch((err) => console.log(err));
};
//reducer
import { FETCH_POPULAR } from "../actions/types";
const initialState = {
popular: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_POPULAR:
return {
...state,
popular: action.payload,
};
default:
return state;
}
}
import React from "react";
import { connect } from "react-redux";
import Popular from "./Popular";
const FetchedPopular = (props) => {
const { popular } = props;
let content = "";
content =
popular.length > 0
? popular.map((item, index) => (
<Popular key={index} popular={item} />
))
: null;
return <div className="fetched-movies">{content}</div>;
};
const mapStateToProps = (state) => ({
popular: state.popular.popular,
});
export default connect(mapStateToProps)(FetchedPopular);
import React from "react";
import "../Styles.css";
const Popular = (props) => {
return (
<div className="movie-container">
<img
className="poster"
src={`https://image.tmdb.org/t/p/w400/${props.poster_path}`}
/>
</div>
);
};
export default Popular;
I cant really tell what I'm missing can someone help?
Next to mapStateToProps you need to create mapDispatchToProps. After that, you will be able to call your Redux action from your React component.
I suggest you the mapDispatchToProps as an Object form. Then you need to use this mapDispatchToProps as the second parameter of your connect method.
When you will have your action mapped to your component, you need to call it somewhere. It is recommended to do it for example on a component mount. As your React components are Functional components, you need to do it in React useEffect hook.
import React, { useEffect } from "react";
import { connect } from "react-redux";
import Popular from "./Popular";
import { fetchPopularMovies } from 'path_to_your_actions_file'
const FetchedPopular = (props) => {
const { popular } = props;
let content = "";
useEffect(()=> {
// call your mapped action (here it is called once on component mount due the empty dependency array of useEffect hook)
props.fetchPopularMovies();
}, [])
content =
popular.length > 0
? popular.map((item, index) => (
<Popular key={index} popular={item} />
))
: null;
return <div className="fetched-movies">{content}</div>;
};
const mapStateToProps = (state) => ({
popular: state.popular.popular,
});
// create mapDispatchToProps
const mapDispatchToProps = {
fetchPopularMovies
}
// use mapDispatchToProps as the second parameter of your `connect` method.
export default connect(mapStateToProps, mapDispatchToProps)(FetchedPopular);
Moreover, as I wrote above in my comment, your Popular does not have the prop poster_path but it has the prop popular which probably has the property poster_path.
import React from "react";
import "../Styles.css";
const Popular = (props) => {
return (
<div className="movie-container">
<img
className="poster"
src={`https://image.tmdb.org/t/p/w400/${props.popular.poster_path}`}
/>
</div>
);
};
export default Popular;
I am learning React/Redux and I am trying to refactor this code from class-based to functional/hooks-based code. The application is an exercise I am working on, it has three components Posts.js where I fetch a list of posts from typicode.com. Each post from the fetched list has a button attacked.
On onClick, it should show details for each post (PostDetails.js and Comments.js):
At the moment, both Posts and Comments are class-based components. I need to:
Step 1: Change them to be functional components and use React Hooks but still keep connect(), mapStateToProps and mapDispatchToProps;
Step 2: Implement React-Redux hooks (UseSelector, useDispatch)
App.js
//imports...
const App = () => {
return (
<div className="container">
<div><Posts /></div>
<div><PostDetails /></div>
</div>
)
}
export default App;
actions
import jsonPlaceholder from '../apis/jsonPlaceholder';
export const fetchPosts = () => async dispatch => {
const response = await jsonPlaceholder.get('/posts');
dispatch({type: 'FETCH_POSTS', payload: response.data})
};
export const selectPost = post => {
return ({
type: 'POST_SELECTED',
payload: post
})
}
export const fetchComments = (id) => async dispatch => {
const response = await jsonPlaceholder.get(`/comments?postId=${id}`);
dispatch({type: 'FETCH_COMMENTS', payload: response.data})
}
reducers
export default (state = [], action) => {
switch (action.type) {
case 'FETCH_POSTS':
return action.payload;
default:
return state;
}
}
export default (selectedPost = null, action) => {
if (action.type === 'POST_SELECTED') {
return action.payload;
}
return selectedPost;
}
export default (state = [], action) => {
switch (action.type) {
case 'FETCH_COMMENTS':
return action.payload;
default:
return state;
}
}
export default combineReducers({
posts: postsReducer,
selectedPost: selectedPostReducer,
comments: commentsReducer
})
components/Posts.js
import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts, selectPost } from '../actions';
import '../styles/posts.scss';
class Posts extends React.Component {
componentDidMount() {
this.props.fetchPosts()
}
renderPosts() {
return this.props.posts.map(post => {
if (post.id <= 10)
return (
<div className='item' key={post.id}>
<div className="title">
<h4>{post.title}</h4>
</div>
<button
onClick={() => {
this.props.selectPost(post)
console.log(post)
}
}>Open</button>
<hr/>
</div>
)
})
}
render() {
return(
<div className="list">
{ this.renderPosts() }
</div>
)
}
}
const mapStateToProps = state => {
return {
posts: state.posts,
selectedPost: state.post
}
};
const mapDispatchToProps = {
fetchPosts,
selectPost
}
export default connect(mapStateToProps, mapDispatchToProps)(Posts);
components/PostDetails.js
import React from 'react';
import { connect } from 'react-redux';
import Comments from './Comments'
const PostDetails = ({ post }) => {
if (!post) {
return <div>Select a post</div>
}
return (
<div className="post-details">
<div className="post-content">
<h3>{post.title}</h3>
<p>{post.body}</p>
<hr/>
</div>
<div className="comments-detail">
<Comments postId={post.id}/>
</div>
</div>
)
}
const mapStateToProps = state => {
return {post: state.selectedPost}
}
export default connect(mapStateToProps)(PostDetails);
components/Comments.js
import React from 'react';
import { connect } from 'react-redux';
import { fetchComments } from '../actions'
class Comments extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.postId && this.props.postId !== prevProps.postId){
this.props.fetchComments(this.props.postId)
}
}
renderComments() {
console.log(this.props.comments)
return this.props.comments.map(comment => {
return (
<div className="comment" key={comment.id}>
<div className="content">
<h5>{comment.name}</h5>
<p>{comment.body}</p>
</div>
<hr />
</div>
)
})
}
render() {
return (
<div className="comments">
{this.renderComments()}
</div>
)
}
}
const mapStateToProps = state => {
return {comments: state.comments}
}
export default connect(mapStateToProps, {fetchComments})(Comments);
This could be a way to create Posts component:
I am assuming that when you dispatch fetchPosts() action, you are saving its response using reducers in Redux.
And, you don't need fetchedPosts in local component state as you already have this data in your Redux state.
const Posts = () => {
const posts = useSelector((state) => state.posts)
const dispatch = useDispatch()
// const [fetchedPosts, setFetchedPosts] = useState([]) // NOT needed
useEffect(() => {
dispatch(fetchPosts())
// setFetchedPosts(posts) // NOT needed
// console.log(posts) // NOT needed, its value may confuse you
}, [])
// Do this, if you want to see `posts` in browser log
useEffect(() => {
console.log(posts)
}, [posts])
/* NOT needed
const renderPosts = () => {
posts.map((post) => {
console.log(post)
})
} */
return (
<>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</>
)
}
export default Posts