I have the next React/Redux/Thunk code:
This is my call to API:
//api.js
export const categoriesFetchData = (page, parentOf) => {
return axios.get(
url +
'categories/' +
'?parent=' +
parentOf +
'&limit=10' +
'&offset=' +
(page - 1) * 10,
);
};
This my action (I pretend to return an axios promise from this action):
//actions.js
export const subCategoriesFetchData = (page = 1, parentOf) => {
return dispatch => {
dispatch(oneCategoryLoading(true));
return api.categoriesFetchData(page, parentOf)
.then(response => {
dispatch(subCategoriesFetchDataSuccess(response.data.results));
dispatch(oneCategoryLoading(false));
})
.catch(error => {
console.log(error);
});
};
};
And in my container:
const mapDispatchToProps = dispatch => {
return {
fetchOneCategory: slug => {
dispatch(fetchOneCategory(slug)).then(() => {
console.log('working');
});
},
};
};
But I get this error:
Uncaught TypeError: Cannot read property 'then' of undefined
What is wrong and how to return a promise in the container?
Thanks for your help.
Here's how I am approaching this.
First, there are a couple of changes you need to do to your subCategoriesFetchData function:
export const subCategoriesFetchData = (page = 1, parentOf) => {
return dispatch => {
dispatch(oneCategoryLoading(true));
// That's the key thing. Return a new promise, then ma
return new Promise((resolve, reject) => {
api.categoriesFetchData(page, parentOf)
.then(response => {
dispatch(subCategoriesFetchDataSuccess(response.data.results));
dispatch(oneCategoryLoading(false));
resolve(response); // Resolve it with whatever you need
})
.catch(error => {
console.log(error);
reject(error); // And it's good practice to reject it on error (optional)
});
});
};
};
Then, here's how you can do the trick with mapDispatchToProps and then chaining .then()s:
import React from 'react';
import { connect } from 'react-redux';
import { subCategoriesFetchData } from './wherever-it-is';
class MyComponent extends React.Component {
componentDidMount() {
this.props.subCategoriesFetchData()
.then( () => { console.log('it works'); })
.catch( () => { console.log('on error'); });
}
render() {
return ( <p>Whatever</p> );
}
}
const mapDispatchToProps = { subCategoriesFetchData };
connect(null, mapDispatchToProps)(MyComponent);
Sorry, i will answer my own question:
i change this:
const mapDispatchToProps = dispatch => {
return {
fetchOneCategory: slug => {
dispatch(fetchOneCategory(slug)).then(() => {
console.log('working');
});
},
};
};
to
const mapDispatchToProps = dispatch => {
return {
fetchOneCategory: slug => {
return dispatch(fetchOneCategory(slug))
},
};
};
and now i can make this:
import React from 'react';
import { connect } from 'react-redux';
class MyComponent extends React.Component {
componentDidMount() {
this.props.fetchOneCategory()
.then( () => { console.log('it works'); })
.catch( () => { console.log('on error'); });
}
render() {
return ( <p>Whatever</p> );
}
}
thanks for your help!
in your container, you don't need
.then(() => {
console.log('working');
});
dispatch(fetchOneCategory(slug)) doesn't return a promise, there is no then to be read
Related
I wrote a hook that calls apollo useQuery. It's pretty simple:
useDecider:
import { useState } from 'react';
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = name => {
const [enabled, setEnabled] = useState(false);
useQuery(GET_DECIDER, {
variables: {
name
},
onCompleted: data => {
const decision = data?.deciders[0]?.decision;
setEnabled(decision);
},
onError: error => {
return error;
}
});
return {
enabled
};
};
I'm trying to test it now and the MockedProvider is not returning the expected data:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
const getMock = (value = false, decider = '') => [
{
request: {
query: GET_DECIDER,
variables: {
name: decider
}
},
result: () => {
console.log('APOLLO RESULT');
return {
data: {
deciders: [
{
decision: value,
name: decider,
value: 10
}
]
}
};
}
}
];
const FakeComponent = ({ decider }) => {
const { enabled } = useDecider(decider);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
const WrappedComponent = ({ decider, value }) => (
<MockedProvider mocks={getMock(value, decider)} addTypename={false}>
<FakeComponent decider={decider} />
</MockedProvider>
);
describe('useDecider', () => {
it('when decider returns true', () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = screen.getByText('isEnabled');
expect(result).toBeInTheDocument();
});
});
I simplified your hook implementation and put together a working example:
import { useQuery, gql } from "#apollo/client";
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { data } = useQuery(GET_DECIDER, { variables: { name } });
return { enabled: data?.deciders[0]?.decision || false };
};
Note that in the test I also updated your getBy to an await findBy:
describe("useDecider", () => {
it("when decider returns true", async () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = await screen.findByText("isEnabled");
expect(result).toBeInTheDocument();
});
});
This is because you need to wait for your API call to complete before the data will be on the page, hence you would not expect the data to be there on the first render.
From https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
To test how your component is rendered after its query completes, you
can await a zero-millisecond timeout before performing your checks.
This delays the checks until the next "tick" of the event loop, which
gives MockedProvider an opportunity to populate the mocked result
try adding before your expect call
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
I'm having trouble fetching a list of users from an api. I think issue might be in my mapDispatchToProps function but I'm not sure. Everything else seems fine to me. I'm new to redux and I'm kinda having a hard time wrapping my head around it so any help is appreciated
The list with the users would ideally be displayed as soon as the component mounts. I did the same thing without redux store and it was working just fine, I'm just not really sure how to integrate redux
Actions
export const startLoading = () => {
return {
type: START_LOADING
}
}
export const updateUserData = payload => {
return {
type: UPDATE_USER_DATA,
payload
}
}
export const updateUserError = payload => {
return {
type: UPDATE_USER_ERROR,
payload: payload
}
}
export function fetchUsers() {
return dispatch => {
dispatch(startLoading());
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
data = data.filter(user => user.id < 4);
data.forEach(user => {
user.isGoldClient = false;
user.salary = '4000';
user.photo = userThumbnail;
})
.then(data => {
dispatch(updateUserData(data));
}).catch(error => {
dispatch(updateUserError(error));
})
});
};
};
Reducers
const initialState = {
loading: false,
users: [],
error: null
};
export function userReducer(state=initialState, action){
switch(action.type){
case START_LOADING:
return {
...state,
loading: true
}
case UPDATE_USER_DATA:
return {
...state,
loading: false,
users: action.payload,
error: null
}
case UPDATE_USER_ERROR:
return {
...state,
error: action.payload,
loading: false,
users: []
};
default:
return state;
};
};
Component
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
users: [],
usersAreDisplayed: true
};
}
componentDidMount() {
fetchUsers();
}
render(){
return (
<UserList users={this.state.users} />
)
}
}
function mapStateToProps(state){
return { users: state.users }
}
function mapDispatchToProps(dispatch){
return {
fetchUsers: payload => dispatch(updateUserData(payload)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Looks like you are not calling the actual fetchUsers at all.
Change the component code like this
function mapStateToProps(state){
return { users: state.users }
}
// remove this function
// function mapDispatchToProps(dispatch){
// return {
// fetchUsers: payload => dispatch(updateUserData(payload)),
// }
// }
export default connect(mapStateToProps, {fetchUsers})(Home); //<---- destructure it here. Also import the function (action)
1a. fetchUsers function needs to be accessed using this.props
componentDidMount() {
this.props.fetchUsers();
}
There is an extra then block after forEach.
Remove it.
export function fetchUsers() {
return (dispatch) => {
dispatch(startLoading());
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
data = data.filter((user) => user.id < 4);
data.forEach((user) => {
user.isGoldClient = false;
user.salary = "4000";
user.photo = userThumbnail;
});
dispatch(updateUserData(data)); // <------ no extra .then is required
})
.catch((error) => {
dispatch(updateUserError(error));
});
};
}
Also <UserList users={this.state.users} /> needs to be <UserList users={this.props.users} /> As already mentioned by #Nsevens
You are mapping redux state into your component's props.
So you should load the users from the component's props and not it's state:
render(){
return (
<UserList users={this.props.users} />
)
}
I am trying to implement Firebase Authentication in my React App.
I have buttons LogIn and LogOut, which are working correctly(i can console.log uid)
Redirecting is fine as well.
Then i tried to add new reducer 'user' : {uid: uid} or 'user': {} if logged out.
At this point my App doesn't want to run and browser shows the notification "A web page is slowing your browser".
Thats GitHub link
App.js:
import ReactDOM from "react-dom";
import Navigation from "../routers/Navigation";
import { firebase, database } from "../firebase/firebase";
import { startSetExpenses, setFiltersText } from "../redux/actions";
import { createBrowserHistory } from "history";
import { connect } from "react-redux";
import {logIn, logOut} from '../redux/actions'
let history = createBrowserHistory();
export function App(props) {
firebase.auth().onAuthStateChanged((user) => {
if (!user) {
props.dispatch(logOut())
history.push("/");
} else {
props.dispatch(logIn(user.uid))
if (history.location.pathname === "/") {
history.push("/dashboard");
}
}
});
return (
<div>
<h1>Expenses App</h1>
<Navigation history={history}/>
</div>
);
}
let mapStoreToProps = (dispatch) => {
return {
dispatch
}
}
export default connect(mapStoreToProps)(App)
RootReducer.js :
const initialState = {
filters: {
text: ""
},
expenses: [],
user: {}
};
function userReducer(state = {}, action) {
const {type, uid} = action
switch (type) {
case "LOGIN":
return {
uid: uid
}
case "LOGOUT":
return {}
default :
return state;
}
}
function expensesReducer(state = initialState.expenses, action) {
const { type,id, description, value, updates,expenses } = action;
switch (type) {
case "ADD_EXPENSE":
return [
...state,
{
id,
description,
value,
},
];
case "REMOVE_EXPENSE":
if (state.findIndex(expense => expense.id === id) < 0) throw new Error('index is not found')
return state.filter((expense) => expense.id !== id);
case "UPDATE_EXPENSE":
return state.map((expense) => {
return expense.id === id ? { ...expense, ...updates } : expense;
});
case "SET_EXPENSES":
if (expenses) {
return expenses
}
default:
return state;
}
}
function filtersReducer(state = initialState.filters, action) {
const {type, text} = action;
switch (type) {
case 'SET_FILTERS_TEXT':
return {...state, text}
default:
return state;
}
}
const rootReducer = combineReducers({
filters: filtersReducer,
expenses: expensesReducer,
user: userReducer
})
export default rootReducer
Actions.js:
import {database, firebase, googleProvider} from "../firebase/firebase";
export function logIn(uid) {
return (dispatch) => {
console.log('uid inside actionCreator', uid)
dispatch({
type: "LOGIN",
uid: uid
})
}
}
export function logOut() {
return (dispatch) => {
console.log('logged out')
dispatch({type: 'LOGOUT'})
}
}
export function startLogIn() {
return () => {
return firebase.auth().signInWithPopup(googleProvider)
}
}
export function startLogOut() {
return () => {
return firebase.auth().signOut()
}
}
export function addExpense({ id, description, value } = {}) {
return {
type: "ADD_EXPENSE",
id,
description,
value,
};
}
export const startAddExpense = (expense) => {
return (dispatch) => {
let newId = database.ref("expenses").push().key;
const { description, value } = expense;
return database
.ref("expenses/" + newId)
.set({ description, value })
.then(() => {
dispatch(
addExpense({
type: "ADD_EXPENSE",
id: newId,
description,
value,
})
);
});
};
};
export function removeExpense(id) {
return {
type: "REMOVE_EXPENSE",
id,
};
}
export const startRemoveExpense = (id) => {
return (dispatch) => {
return database
.ref("expenses/" + id)
.remove()
.then(() => {
console.log("removing expense with id : " + id);
dispatch(removeExpense(id));
})
.catch((error) => {
console.log(`Expense with id:${id} was not removed`);
console.log(error)
});
};
};
export function updateExpense(id, updates) {
return {
type: "UPDATE_EXPENSE",
id,
updates,
};
}
export const startUpdateExpense = (id, updates) => {
return (dispatch) => {
return database.ref('expenses/' + id)
.update(updates)
.then(() => {
dispatch(updateExpense(id, updates))
})
.catch((err) => {
console.log('Error with updating an expense from firebase')
console.log(err)
})
}
}
export function setFiltersText(text) {
return {
type: "SET_FILTERS_TEXT",
text,
};
}
export const setExpenses = (expenses) => {
return {
type: "SET_EXPENSES",
expenses: [...expenses],
};
};
export const startSetExpenses = () => {
return (dispatch) => {
//get expenses from database
//.then dispatch expenses to state with setExpenses
return database
.ref("expenses")
.once("value")
.then((snapshot) => {
const expensesObj = snapshot.val();
let expenses = [];
for (let property in expensesObj) {
expenses = [
...expenses,
{
id: property,
description: expensesObj[property].description,
value: expensesObj[property].value,
},
];
}
dispatch(setExpenses(expenses));
});
};
};
I'm fetching data from an endpoint. But the state is not updated. it's always undefined.
For some reason this.props.users is undefined. Am I doing something wrong?
After componentDidMount() I trigger the action fetchUsers that send a request to the endpoint. The data is fetched successfully but at the end the state is not updated.
This is my Layout component
class Layout extends Component {
render() {
return (
<div className="container">
{
this.props.users.map((user, key) => {
return <a className="list-group-item list-group-item-action active">User #{user.id}</a>
})
}
</div>
)
}
}
const mapStateToProps = state => {
return {
channels: state.users.data,
}
}
const mapDispatchToProps = dispatch => {
return {
fetchUsers: () =>
dispatch(user.fetchUsers()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Layout);
This the action file
export const fetchUsers = () => {
return (dispatch, getState) => {
let headers = { "Content-Type": "application/json" };
return fetch("http://127.0.0.1:3030/api/users/", { headers, })
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
return { status: res.status, data };
})
} else {
console.log("Server Error!");
throw res;
}
})
.then(res => {
if (res.status === 200) {
dispatch({ type: 'USERS_FETCHED', data: res.data });
return res.data;
}
})
}
}
And this is the reducer
const initialState = {
users: []
};
export default function channels(state = initialState, action) {
switch (action.type) {
case 'USERS_FETCHED':
return { ...state, users: action.data };
default:
return state;
}
}
I think the error comes from your call to the dispatcher in the mapDispatchToProps. Since you are exporting directly the function fetchUsers, you should not be calling user.fetchUsers.
const mapDispatchToProps = dispatch => {
return {
fetchUsers: () =>
dispatch(fetchUsers()),
}
}
I can't render an array of objects into my view using JSX. It does not load on the screen.
I've been learning React and I can render an array of strings, but not the array of objects.
Here is my component:
import React, { Component } from "react";
export default class PokedexGridComponent extends Component {
constructor(props) {
super(props);
console.log(props);
this.state = {
pokemons: [],
all: []
};
}
componentDidMount() {
this.getPokemons();
}
render() {
return (
<div>
<input
className="btn btn-success btn-sm mb-5"
type="button"
onClick={this.getPokemons}
value="Buscar Pokemons"
/>
<div>
{this.state.all.map(data => {
return <li key={data.key}>{data.name}</li>;
})}
</div>
</div>
);
}
getPokemons = () => {
var pokemon = [];
fetch("https://pokeapi.co/api/v2/pokemon?offset=20&limit=964")
.then(data => {
return data.json();
})
.then(data => {
data["results"].forEach(data => {
pokemon.push(data.name.charAt(0).toUpperCase() + data.name.slice(1));
});
this.setState({ pokemons: pokemon });
return this.state.pokemons;
})
.then(data => {
var tmp = [];
this.state.pokemons.forEach((data, idx) => {
fetch(`https://pokeapi.co/api/v2/pokemon/${data.toLowerCase()}`)
.then(data => {
return data.json();
})
.then(data => {
tmp.push({
name: data.name,
image: data.sprites.front_default,
key: idx
});
});
});
this.setState({ all: tmp });
console.log(this.state.all);
});
};
}
The console returns the array of objects but can't map it on render method.
May anyone help me?
The setState method is async so if you need to do something after updated the state you would need to use the second parameter that is a function that will be executed after updating the state.
this.setState({ pokemons: pokemon }, function(){
//perform what you need with the updated
})
Another issue you are having is that you are updating before the requests have arrived. You can collect all your promises and apply them Promise.all:
const requests = []
pokemons.forEach((data, idx) => {
const request = fetch(`https://pokeapi.co/api/v2/pokemon/${data.toLowerCase()}`)
.then((data) => {
return data.json();
})
requests.push(request)
})
const tmp = [];
Promise.all(request).then((arrayOfResults) => {
//Here you have the responses to iterate
arrayOfResults.forEach((data) => {
tmp.push({
name: data.name,
image: data.sprites.front_default,
key: idx
})
})
this.setState({all: tmp}, function(){
//here you can see the state after update
})
})
Works with Promise.all, thank you #SergioEscudero and #Vencovsky.
Here is the modified code:
import React, { Component } from 'react'
export default class PokedexGridComponent extends Component {
constructor(props) {
super(props);
this.state = {
pokemons: [],
pokeComplete: []
}
}
componentDidMount() {
this.getPokemons();
}
render() {
return (
<div>
<input className="btn btn-success btn-sm mb-5" type="button" onClick={this.getPokemons} value="Buscar Pokemons" />
<div>
{
(this.state.pokeComplete.map((data) => {
return (<li>{data.name} | {data.img}</li>)
}))
}
</div>
</div>
)
}
getPokemons = () => {
var pokemons = [];
fetch("https://pokeapi.co/api/v2/pokemon?offset=20&limit=964")
.then((data) => {
return data.json();
})
.then((data) => {
data["results"].forEach((data) => {
pokemons.push(data.name)
})
return pokemons;
}).then((data) => {
var arrayReq = [];
data.forEach((x) => {
var req = fetch(`https://pokeapi.co/api/v2/pokemon/${x.toLowerCase()}`)
.then((data) => {
return data.json();
})
arrayReq.push(req);
})
var tmp = [];
Promise.all(arrayReq)
.then((e) => {
e.forEach((x) => {
tmp.push({
name: x.name,
img: x.sprites.front_default
})
})
})
.then(() => {
this.setState({ pokeComplete: tmp });
})
})
}
}