I need to get the data from the server and save it somewhere so that after re-rendering the LoginsList component, I don't have to request the data from the server again. I decided to start using MobX, but the store function fetchData() just doesn't seem to get called.
For now, the data is accepted in raw form, but then I will use encryption.
store.js
import { makeObservable } from 'mobx';
class store {
data = []
isFetching = false
error = null
constructor() {
makeObservable(this, {
data: observable,
isFetching: observable,
error: observable,
fetchData: action
})
}
fetchData() {
this.isFetching = true
axios.get('http://localhost:3001/data')
.then(response => {
this.data = response.data
this.isFetching = false
console.log('Success');
})
.catch(err => {
this.error = err
this.isFetching = false
console.log('Error');
})
}
}
export default store;
LoginsList.js
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Observer } from 'mobx-react-lite';
import store from '../.store/data';
import LoginDetails from './LoginDetails';
import Login_Icon from '../assets/icons/Login.svg'
import '../assets/css/LoginCards.css'
const LoginsList = () => {
const [activeTab, setActiveTab] = useState(0);
const [hoveredTab, setHoveredTab] = useState(null);
const handleMouseEnter = (index) => {
if (index !== activeTab) {
setHoveredTab(index);
}
}
const handleClick = (index) => {
setHoveredTab(null);
setActiveTab(index);
}
useEffect(() => {
store.fetchData();
}, []);
return (
<>
<Observer>
<ul>
{store.data.map((card, index) => (
<li
key={card.id}
onClick={() => handleClick(index)}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => setHoveredTab(null)}
className="LoginCard"
>
<div
className={classNames('LoginCardContainer', { 'active-logincard': index === activeTab }, { 'hovered-logincard': index === hoveredTab })}
>
<img src={Login_Icon} alt="Login Icon"></img>
<div className="TextZone">
<p>{card.name}</p>
<div>{card.username}</div>
</div>
</div>
</li>
))}
</ul>
<div>
<div className="LoginDetails">
<img className="LoginDetailsIcon" src={Login_Icon}></img>
</div>
<LoginDetails key={activeTab} name={store.data[activeTab].name} username={store.data[activeTab].username} password={store.data[activeTab].password}/>
{store.data[activeTab].password}
</div>
</Observer>
</>
);
}
export default LoginsList;
I tried creating a store, importing it into the LoginsList component and getting the data. In the browser console, I saw an error Uncaught TypeError: _store_data__WEBPACK_IMPORTED_MODULE_3__.default.data is undefined
If I open http://localhost:3001/data in my browser, I can easily read the data. I don't think the error is on the server side.
I solved the problem. All I had to do was use makeAutoObservable instead of makeObservable.
import { action, makeAutoObservable } from 'mobx';
import axios from 'axios';
class UserData {
data = []
isFetching = false
error = null
constructor() {
makeAutoObservable(this)
}
fetchData() {
this.isFetching = true
axios.get('http://localhost:3001/data')
.then(response => {
this.data = response.data
this.isFetching = false
console.log('Success');
})
.catch(err => {
this.error = err
this.isFetching = false
console.log('Error');
})
};
}
export default new UserData;
Related
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.
import React, {useState, useEffect} from 'react';
import {connect} from 'react-redux';
import {
fetchRecipes
} from '../../store/actions';
import './BeerRecipes.css';
const BeerRecipes = ({recipesData, fetchRecipes}) => {
useEffect(() => {
fetchRecipes();
}, [])
return (
<div className='beer_recipes_block'>
<div className='title_wrapper'>
<h2 className='title'>Beer recipes</h2>
</div>
<div className='beer_recipes'>
<ul className='beer_recipes_items'>
{
recipesData && recipesData.recipes &&.recipesData.recipes.map(recipe =>
<li className='beer_recipes_item' id={recipe.id}>{recipe.name}</li>
)
}
</ul>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
recipesData: state.recipes
}
}
const mapDispatchToProps = dispatch => {
return {
fetchRecipes: () => dispatch(fetchRecipes())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerRecipes);
this is my component where I would like to create infinite scroll and below is my redux-action with axios:
import axios from "axios";
import * as actionTypes from "./actionTypes";
export const fetchRecipesRequest = () => {
return {
type: actionTypes.FETCH_RECIPES_REQUEST
}
}
export const fetchRecipesSuccess = recipes => {
return {
type: actionTypes.FETCH_RECIPES_SUCCESS,
payload: recipes
}
}
export const fetchRecipesFailure = error => {
return {
type: actionTypes.FETCH_RECIPES_FAILURE,
payload: error
}
}
export const fetchRecipes = (page) => {
return (dispatch) => {
dispatch(fetchRecipesRequest)
axios
.get('https://api.punkapi.com/v2/beers?page=1')
.then(response => {
const recipes = response.data;
dispatch(fetchRecipesSuccess(recipes));
})
.catch(error => {
const errorMsg = error.message;
dispatch(fetchRecipesFailure(errorMsg));
})
}
}
I want to create a scroll. I need, firstly, to display first 10 elements and then to add 5 elements with every loading. I have 25 elements altogether and when the list is done it should start from the first five again.
I am trying to create a component that will search a REST API through an axios request, and then return a list of the results. Right now, I'm facing an issue where all I am getting when I search is 'undefined' and I have no clue why. Any and all suggestions would be amazing.
Users.js
import React, { Component } from 'react';
import axios from 'axios';
import { search } from './utils';
import Users from './UsersDelete';
class App extends Component {
state = {
users: null,
loading: false,
value: ''
};
search = async val => {
this.setState({ loading: true });
const res = await search(
`https://zuul-stage.whatifops.com/v1/user/email/${val}`
);
const users = await res.data.results;
this.setState({ users, loading: false });
};
onChangeHandler = async e => {
this.search(e.target.value);
this.setState({ value: e.target.value });
};
get renderUsers() {
let users = <h1>There's no movies</h1>;
if (this.state.movies) {
users = <Users list={this.state.users} />;
}
return users;
}
render() {
return (
<div>
<input
value={this.state.value}
onChange={e => this.onChangeHandler(e)}
placeholder='Type something to search'
/>
{this.renderUsers}
</div>
);
}
}
export default App;
User.js
import React from 'react';
import { truncStr } from './utils';
const User = props => {
const { id, email, phone } = props.item;
return (
<div className={classes.Container}>
<div className={classes.VoteContainer}>
<span className={classes.Vote}>{email}</span>
</div>
<div className={classes.Bottom}>
<h3 className={classes.Title}>{truncStr(phone, 19)}</h3>
</div>
</div>
);
};
export default User;
UsersDelete.js
import React from 'react';
import User from './User';
const Users = ({ list }) => {
let cards = <h3>Loading...</h3>;
if (list) {
cards = list.map((m, i) => <User key={i} item={m} />);
}
return (
<div>
<div>{cards}</div>
</div>
);
};
export default Users;
utils.js
import axios from 'axios';
export const truncStr = (string, limit) => {
return string.length > limit
? string
.trim()
.substring(0, limit - 3)
.trim() + '...'
: string;
};
const resources = {};
const makeRequestCreator = () => {
let cancel;
return async query => {
if (cancel) {
// Cancel the previous request before making a new request
cancel.cancel();
}
// Create a new CancelToken
cancel = axios.CancelToken.source();
try {
if (resources[query]) {
// Return result if it exists
return resources[query];
}
const res = await axios(query, { cancelToken: cancel.token });
const result = res.data.results;
// Store response
resources[query] = result;
return result;
} catch (error) {
if (axios.isCancel(error)) {
// Handle if request was cancelled
console.log('Request canceled', error.message);
} else {
// Handle usual errors
console.log('Something went wrong: ', error.message);
}
}
};
};
export const search = makeRequestCreator();
**Update: This is the response info after I called console.log(res) after the search function
A few things wrong with your code:
There is no results property on the data returned from the REST API
You don't need to await on this line: const users = await res.data.results;
There is no movies property on your state
I created a codesandbox to test your solution, here is an updated version: https://codesandbox.io/s/async-browser-tz4p6
I have removed a few things from the User.js file that were not necessary (for my tests)
Sorry for my bad english and my bad logic, i wanted use pokeapi.co for my Pokedex.
My problem : i need display the props who content the informations from api. I can receive informations in my console.log(response) on my axios request, but i can not display them on a list on my Nav component, where are my errors ?
The endPoint : https://pokeapi.co/api/v2/pokemon
My code :
App.js
// == Import : npm
import React from 'react';
// == Import : local
import Home from 'src/components/Home';
import Nav from 'src/containers/Nav';
import './app.scss';
// == Composant
const App = results => (
<div id="app">
<nav className="nav">
<Nav props={results} />
</nav>
<main className="content">
<Home />
</main>
)}
</div>
);
// == Export
export default App;
reducer.js
// == Initial State
const initialState = {
results: [],
name: '',
url: '',
};
// == Types
export const FETCH_POKEMON_API = 'FETCH_POKEMON_API';
const RECEIVE_POKEMON_LIST = 'RECEIVE_POKEMON_LIST';
// == Reducer
const reducer = (state = initialState, action = {}) => {
// console.log('une action arrive dans le reducer', action);
switch (action.type) {
case RECEIVE_POKEMON_LIST:
return {
...state,
results: action.results,
};
default:
return state;
}
};
// == Action Creators
export const fetchPokemonApi = () => ({
type: FETCH_POKEMON_API,
});
export const receivePokemonList = results => ({
type: RECEIVE_POKEMON_LIST,
results,
});
// == Selectors
// == Export
export default reducer;
Nav Component
import React from 'react';
import PropTypes from 'prop-types';
// import { NavLink } from 'react-router-dom';
// import { getUrl } from 'src/utils';
import './nav.scss';
const Nav = ({ results }) => {
console.log('if i receive my props from PokeAPI its win guys', results);
return (
<div className="menu">
{results.map(({ Pokemon }) => (
<li key={Pokemon} className="menu-item">
{Pokemon}
</li>
))}
</div>
);
};
Nav.propTypes = {
results: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
}),
).isRequired,
};
export default Nav;
Nav Container
// == Import : npm
import { connect } from 'react-redux';
// == Import : local
import Nav from 'src/components/Nav';
// === State (données) ===
const mapStateToProps = state => ({
results: state.results,
});
// === Actions ===
const mapDispatchToProps = {};
// Container
const NavContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(Nav);
// == Export
export default NavContainer;
axiosMiddleware
import axios from 'axios';
import { FETCH_POKEMON_API, receivePokemonList } from 'src/store/reducer';
const ajaxMiddleware = store => next => (action) => {
console.log('L\'action suivante tente de passer', action);
switch (action.type) {
case FETCH_POKEMON_API:
axios.get('https://pokeapi.co/api/v2/pokemon')
.then((response) => {
console.log('response', response);
console.log('response.data', response.data);
console.log('response.data.results', response.data.results);
const { data: results } = response;
this.setState({
results: response.data.results,
});
store.dispatch(receivePokemonList(results));
})
.catch(() => {
console.log('Une erreur s\'est produite');
});
break;
default:
// console.log('action pass', action);
next(action);
}
};
export default ajaxMiddleware;
whats wrong ?
Thanks you !
Thanks to #Meshack Mbuvi for his support in private.
The abscence of thunk is the first problem
The following code resolved my problem ( thanks to #Mbuvi ) :
axios.get('https://pokeapi.co/api/v2/pokemon')
.then((response) => {
console.log('response.data.results', response.data.results);
// ici je défni pokemons from api, qui contient la list des pokemons
// It was like this: const {results} = response.data.results
const { results } = response.data; // The error was here
dispatch(receivePokemonList(results));
})
.catch((err) => {
console.log('Une erreur s\'est produite', err);
});
};
I am thinking the code is failing because initially, the results are undefined(Before the call to the API). Also, your Nav component requires that your result object should have a property named name.
Have your initial state as follows:
const initialState = {
results: [{name:""}],
name: '',
url: '',
};
Let me know what how it goes
Using Lodash
{
_.map(results, (Pokemon, index) => {
return (
<li key={Pokemon} className="menu-item">
{Pokemon}
</li>
)})
}
Using Native Map
{
results.map((Pokemon, index) => {
return (
<li key={Pokemon} className="menu-item">
{Pokemon}
</li>
)})
}
I think you forgot to return the map component.
The imageList is empty even though I have used this.context.setImageList to update the state to get the data from the API. I am confused why the state just didn't update. I have been spending a lot of time on this and haven't found the root cause. I'd appreciate if you can guide me through it. Thank you!
Create context and attach it in the Provider
import React, { Component } from 'react';
const ImageListContext = React.createContext({
imageList: [],
error: null,
loading: true,
setError: () => {},
clearError: () => {},
setImageList: () => {},
})
export default ImageListContext
export class ImageListProvider extends Component {
state = {
imageList: [],
error: null,
};
setImageList = imageList => {
this.setState({imageList})
}
setError = error => {
console.error(error)
this.setState({ error })
}
clearError = () => {
this.setState({ error: null })
}
render() {
const value = {
imageList: this.state.imageList,
error: this.state.error,
setError: this.setError,
clearError: this.clearError,
setImageList: this.setImageList,
}
return (
<div>
{this.loading ? <div>Loading Images...</div> :
<ImageListContext.Provider value={value}>
{this.props.children}
</ImageListContext.Provider>}
</div>
)
}
}
Use Context to update imageList array with the data from API and get the data out of the array to display it
import React, { Component } from 'react'
import ImageApiService from '../../services/image-api-service'
import { Section } from '../../components/Utils/Utils'
import ImageListItem from '../../components/ImageListItem/ImageListItem'
import './ImageListPage.css'
import ImageListContext from '../../contexts/ImageListContext'
export default class ImageListPage extends Component {
static contextType = ImageListContext;
componentDidMount() {
//this calls the image API to get all images!
ImageApiService.getImages()
.then(resJson => this.context.setImageList(resJson))
.catch(error => console.log(error))
}
setError = error => {
console.error(error)
this.setState({ error })
}
clearError = () => {
this.setState({ error: null })
}
renderImages() {
const { imageList=[] } = this.context;
console.log(imageList)
return imageList.map(image =>
<ImageListItem
key={image.id}
image={image}
/>
)
}
render() {
return (
<Section list className='ImageListPage'>
{this.context.error
? <p className='red'>There was an error, try again</p>
: this.renderImages()}
</Section>
)
}
}