I am learning React/Redux and came to a point where I am stuck. In the sample todo app I am working on when a new todo is added the addTodo action is taken and I can step through the store.dispatch logic. What fails is the haveStatePropsChanged value is calculated as false hence no child updates.
The code snippets follow:
import React from 'react';
import { connect } from 'react-redux';
import { store, addTodo, completeTodo, deleteTodo, clearTodo } from './TodoState.jsx';
class AddTodoForm extends React.Component {
...
}
class TodoItem extends React.Component {
....
}
let TodoList = ({items}) => (
<ul>
{items.map((item,index) =>
<TodoItem key={index} index={index} message={item.message} completed={item.completed}/>
)}
</ul>
)
let TodoComponent = ({ items, onAddTodo, onCompleteTodo, onDeleteTodo, onClearTodo }) => /* expand's props */
(
<div>
<h1>Todo</h1>
<AddTodoForm onAddTodo={onAddTodo} message/>
<TodoList items={items} onCompleteTodo={onCompleteTodo} onDeleteTodo={onDeleteTodo} onClearTodo={onClearTodo}/>
</div>
)
const mapStateToProps = (state) => {
return {
items: state.todo.items
}
}
const mapDispatchToProps = (dispatch) => {
return {
onAddTodo(message) {
dispatch(addTodo(message))
},
onCompleteTodo(index) {
dispatch(completeTodo(index))
},
onDeleteTodo(index) {
dispatch(deleteTodo(index))
},
onClearTodo(index) {
dispatch(clearTodo(index))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(TodoComponent);
The AddTodoForm correctly dispatches addTodo action, the issue is the TodoList component does not render again even through the items array is a new array.
UPDATE:
My reducer does return a new state.
Here is the reducer and action code:
import { createStore } from 'redux';
var defaultState = { todo: { items: [] } }
const ADD_TODO = 1;
const COMPLETE_TODO = 2;
const DELETE_TODO = 3;
const CLEAR_TODO = 4;
const addTodo = (message) => { return {type: ADD_TODO, message: message, completed: false} };
const completeTodo = (index) => { return {type: COMPLETE_TODO, index:index} };
const deleteTodo = (index) => { return {type: DELETE_TODO, index:index} };
const clearTodo = (index) => { return {type: CLEAR_TODO, index:index} };
function todoReducer(state,action) {
switch(action.type) {
case ADD_TODO:
var newState = Object.assign({},state);
newState.todo.items.push({message:action.message,completed:false});
return newState;
case COMPLETE_TODO:
var newState = Object.assign({},state);
newState.todo.items[action.index].completed = true;
return newState;
case DELETE_TODO:
var items = [].concat(state.todo.items);
items.splice(action.index,1);
return Object.assign({},state,{
todo: {
items:items
}
});
case CLEAR_TODO:
return Object.assign({},state,{
todo: {
items: []
}
});
default:
return state;
}
}
var store = createStore(todoReducer,defaultState);
export { store, addTodo, completeTodo, deleteTodo, clearTodo };
Thanks,
Aaron
Check that you return a new object as a state in your reducer.
Ex:
return Object.assign ({}, state, {items: [...oldItems, newItem]})
Pay attention here [...oldItems, newItem] this will create new array. In your case Object.assign is doing only shallow copy and actually items changed but holds the same reference. Have a look at working example:
import React from 'react';
import { render } from 'react-dom';
import { connect, Provider } from 'react-redux';
import { createStore } from 'redux';
var defaultState = { todo: { items: [] } }
const ADD_TODO = 1;
const COMPLETE_TODO = 2;
const DELETE_TODO = 3;
const CLEAR_TODO = 4;
const addTodo = (message) => { return {type: ADD_TODO, message: message, completed: false} };
const completeTodo = (index) => { return {type: COMPLETE_TODO, index:index} };
const deleteTodo = (index) => { return {type: DELETE_TODO, index:index} };
const clearTodo = (index) => { return {type: CLEAR_TODO, index:index} };
function todoReducer(state,action) {
switch(action.type) {
case ADD_TODO:
var newItem = {message:action.message,completed:false};
return Object.assign({},state, {todo: {items: [...state.todo.items, newItem]}});
case COMPLETE_TODO:
var newState = Object.assign({},state);
newState.todo.items[action.index].completed = true;
return newState;
case DELETE_TODO:
var items = [].concat(state.todo.items);
items.splice(action.index,1);
return Object.assign({},state,{
todo: {
items:items
}
});
case CLEAR_TODO:
return Object.assign({},state,{
todo: {
items: []
}
});
default:
return state;
}
}
var store = createStore(todoReducer,defaultState);
class AddTodoForm extends React.Component {
render() {
return <button onClick={this.props.onAddTodo}>test</button>
}
}
class TodoItem extends React.Component {
render() {
return <span>item</span>
}
}
let TodoList = ({items}) => (
<ul>
{items.map((item,index) =>
<TodoItem key={index} index={index} message={item.message} completed={item.completed}/>
)}
</ul>
)
let TodoComponent = ({ items, onAddTodo, onCompleteTodo, onDeleteTodo, onClearTodo }) => /* expand's props */
(
<div>
<h1>Todo</h1>
<AddTodoForm onAddTodo={onAddTodo} message/>
<TodoList items={items} onCompleteTodo={onCompleteTodo} onDeleteTodo={onDeleteTodo} onClearTodo={onClearTodo}/>
</div>
)
const mapStateToProps = (state) => {
return {
items: state.todo.items
}
}
const mapDispatchToProps = (dispatch) => {
return {
onAddTodo(message) {
dispatch(addTodo(message))
},
onCompleteTodo(index) {
dispatch(completeTodo(index))
},
onDeleteTodo(index) {
dispatch(deleteTodo(index))
},
onClearTodo(index) {
dispatch(clearTodo(index))
}
}
}
var Wrapper = connect(mapStateToProps,mapDispatchToProps)(TodoComponent);
render(
<Provider store={store}>
<Wrapper />
</Provider>,
document.getElementById('app')
)
Related
I'm trying to use a custom hook that bring me functions to handle my TODOS on my context, but it gives me an error
Uncaught TypeError: useContext(...) is undefined
The above error occurred in the component:
Complete Error Image
TodoProvider.jsx
import { useReducer } from 'react';
import { useTodos } from '../hooks/useTodos';
import { TodoContext, todoReducer } from './';
export const TodoProvider = ({ children }) => {
const init = () => {
return [];
};
const [todos, dispatchTodos] = useReducer(todoReducer, {}, init);
const { handleNewTodo, handleToggleTodo } = useTodos();
return (
<TodoContext.Provider
value={{ todos, dispatchTodos, handleNewTodo, handleToggleTodo }}
>
{children}
</TodoContext.Provider>
);
};
useTodos.js
import { useContext } from 'react';
import { TodoContext } from '../context';
import { types } from '../types/types';
export const useTodos = () => {
const { dispatchTodos } = useContext(TodoContext);
const handleNewTodo = todo => {
const action = {
type: types.add,
payload: todo,
};
dispatchTodos(action);
};
const handleToggleTodo = id => {
dispatchTodos({
type: types.toggle,
payload: id,
});
};
return { handleNewTodo, handleToggleTodo };
};
The error traceback in your image says
`useContext(...)` is not defined
useTodos (useTodos.js:6)
Since you aren't showing your useTodos.js file, I must rely on my crystal ball to tell me that you've forgotten to
import {useContext} from 'react';
in useTodos.js, hence "not defined".
Here's an one-file example based on your code that verifiably does work...
import { useReducer, useContext, createContext } from "react";
function todoReducer(state, action) {
switch (action.type) {
case "add":
return [...state, { id: +new Date(), text: action.payload }];
default:
return state;
}
}
const TodoContext = createContext([]);
const TodoProvider = ({ children }) => {
const [todos, dispatchTodos] = useReducer(todoReducer, null, () => []);
return (
<TodoContext.Provider value={{ todos, dispatchTodos }}>
{children}
</TodoContext.Provider>
);
};
function useTodoActions() {
const { dispatchTodos } = useContext(TodoContext);
function handleNewTodo(todo) {
dispatchTodos({
type: "add",
payload: todo
});
}
function handleToggleTodo(id) {
dispatchTodos({
type: "toggle",
payload: id
});
}
return { handleNewTodo, handleToggleTodo };
}
function useTodos() {
return useContext(TodoContext).todos;
}
function TodoApp() {
const todos = useTodos();
const { handleNewTodo } = useTodoActions();
return (
<div>
{JSON.stringify(todos)}
<hr />
<button onClick={() => handleNewTodo((+new Date()).toString(36))}>
Add todo
</button>
</div>
);
}
export default function App() {
return (
<TodoProvider>
<TodoApp />
</TodoProvider>
);
}
I'm trying to fetch data from firebase, then update the state of the app with the results and display the data as a list in a list component.
Everything works except the final list component displays it and immediately becomes blank again. After debugging, I found out it doesn't manage to correctly map the state to the props but I couldn't figure out how to achieve this. Thanks in advance
PlantList.js
import React, { Component } from 'react';
import PlantSummary from './PlantSummary';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { fetchMyPlants } from '../../store/actions/myPlantsActions'
var i =0;
class PlantList extends Component {
constructor(props) {
super(props);
this.state = { myPlants: []} ;
}
componentDidMount() {
console.log("componentDidMount() triggered & state",i,this.state);
console.log("componentDidMount() triggered & props ",i,this.props);
this.props.dispatch(fetchMyPlants());
}
render(){
i = i +1;
console.log("render()"+i,this.props,this.state);
const { myPlants } = this.props;
return(
<div className="plant-list section">
{myPlants && myPlants.map((plant) => {
return (
<Link to={'/plant/'+ plant.id}>
<PlantSummary plant={plant} key={plant.id} />
</Link>
)
})}
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("mapStateToProps triggered",state);
return {
myPlants: state.myPlants.items
}
}
export default connect(mapStateToProps)(PlantList)
myPlantActions.js
export const FETCH_MY_PLANTS_BEGIN = 'FETCH_MY_PLANTS_BEGIN';
export const FETCH_MY_PLANTS_SUCCESS = 'FETCH_MY_PLANTS_SUCCESS';
export const FETCH_MY_PLANTS_FAILURE = 'FETCH_MY_PLANTS_FAILURE';
export const fetchMyPlantsBegin = () => ({
type: FETCH_MY_PLANTS_BEGIN
});
export const fetchMyPlantsSuccess = myPlants => ({
type: FETCH_MY_PLANTS_SUCCESS,
payload: { myPlants }
})
export const fetchMyPlantsFailure = err => ({
type: FETCH_MY_PLANTS_FAILURE,
payload: { err }
});
export const fetchMyPlants = () => {
return(dispatch, getState, { getFirestore }) => {
dispatch(fetchMyPlantsBegin());
const firestore = getFirestore();
const authID = getState().firebase.auth.uid;
const usersPlants = [];
firestore.collection('users').doc(authID).collection('myPlants').get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
firestore.collection('plants').doc(doc.data().id).get().then(
function(document) {
if (document.exists) {
const docToPushId = {id: doc.data().id};
let docToPush = {
...docToPushId,
...document.data()
};
usersPlants.push(docToPush);
} else {
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
}
);
});
}).then(myPlants => {
console.log("Dispatch happens now:",usersPlants);
dispatch(fetchMyPlantsSuccess(usersPlants));
return myPlants;
}).catch(error => dispatch(fetchMyPlantsFailure(error)));
}
};
myPlantsReducer.js
import {
FETCH_MY_PLANTS_BEGIN,
FETCH_MY_PLANTS_SUCCESS,
FETCH_MY_PLANTS_FAILURE
} from '../actions/myPlantsActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function myPlantsReducer(state = initialState, action) {
switch(action.type) {
case 'FETCH_MY_PLANTS_BEGIN':
return {
...state,
loading: true,
error: null
};
case 'FETCH_MY_PLANTS_SUCCESS':
return {
...state,
loading: false,
items: action.payload.myPlants
};
case 'FETCH_MY_PLANTS_FAILURE':
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
console logs
I recently started using redux for a new personal project. It worked pretty well until I started using "combineReducers". Whenever I click "Fetch todos" both my user as well as my todo reducer get updated and even though they have different data field names both get the same data. Now I probably did some wrong encapsulation here. But no matter how often I went over the docs, I just cannot see what I am doing wrong.
My store initialization script:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import toDoReducer from './todos/reducer';
import userReducer from './users/reducer';
const rootReducer = combineReducers({
todosSlice: toDoReducer,
usersSlice: userReducer
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
gets injected into index:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/app/App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
ReactDOM.render(<Provider store={ configureStore }><App /></Provider>, document.getElementById('root'));
serviceWorker.unregister();
My app hold the logic for the todo container
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as todoActions from '../../store/todos/actions';
import UserContainer from '../usersContainer/UserContainer';
class App extends Component {
componentDidMount() {
console.log(this.props);
}
render() {
let loading = '';
let error = '';
let todos = [];
// check whether the component is fetching data
this.props.loading === true ? loading = <p>Loading...</p> : loading = '';
// check if there was an error
this.props.error && this.props.loading === false ? error = <p>There was an error</p> : error = '';
// map the todos in the desired html markup.
todos = this.props.todos.map( todo => {
return <div key={todo.id}> name: {todo.title} </div>
});
return (
<div className="App">
{/* <UserContainer /> */}
{loading}
{error}
<p onClick={() => this.props.onFetchTodos()}>Fetch Todos</p>
{todos}
</div>
);
}
}
const mapStateToProps = state => {
return {
error: state.todosSlice.error,
loading: state.todosSlice.loading,
todos: state.todosSlice.todos
}
}
const mapDispatchToProps = dispatch => {
return {
onFetchTodos: () => dispatch(todoActions.fetchTodos())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Which has the following actions:
import axios from 'axios';
export const FETCH_TODOS = 'FETCH_TODOS';
export const GET_TODOS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchRequest = () => {
return dispatch => {
dispatch(getTodoStarted());
axios.get('https://one365-api-dev.azurewebsites.net/api/teams/')
.then(result => {
dispatch(fetchTodosSucces(result));
}).catch(error => {
dispatch(fetchTodoFailure(error));
});
}
}
const getTodoStarted = () => ({
type: GET_TODOS_STARTED
});
const fetchTodosSucces = todos => ({
type: FETCH_TODOS_SUCCESS,
payload: {
...todos
}
});
const fetchTodoFailure = error => ({
type: FETCH_TODOS_FAILURE,
payload: {
error
}
});
export const fetchTodos = () => {
return (dispatch => {
dispatch(fetchRequest());
});
}
And it's reducer
import * as actions from './actions';
const initialState = {
error: null,
loading: false,
todos: []
}
const todosReducer = (state = initialState, action) => {
switch(action.type) {
case actions.GET_TODOS_STARTED: {
console.log('fetch todo state', state)
return {
...state,
loading: state.loading = true
};
}
case actions.FETCH_TODOS_SUCCESS: {
const todos = action.payload.data;
return {
...state,
loading: false,
todos: state.todos = todos
};
}
case actions.FETCH_TODOS_FAILURE: {
const error = action.payload.error;
return {
...state,
loading: false,
error: state.error = error
};
}
default: {
return state;
}
}
}
export default todosReducer;
The Users Component
import React from 'react';
import { connect } from 'react-redux';
import * as userActions from '../../store/users/actions';
class UserContainer extends React.Component {
render () {
let loading = '';
let error = '';
let users = [];
// check whether the component is fetching data
this.props.usersLoading === true ? loading = <p>Loading...</p> : loading = '';
// check if there was an error
this.props.usersError && this.props.loading === false ? error = <p>There was an error</p> : error = '';
// map the users in the desired html markup.
users = this.props.users.map( user => {
return <div key={user.id}> name: {user.title} </div>
});
return (
<div className="Users">
{loading}
{error}
<p onClick={() => this.props.onFetchUsers()}>Fetch Users</p>
{users}
</div>
);
}
}
const mapStateToProps = state => {
return {
usersError: state.usersSlice.error,
usersLoading: state.usersSlice.loading,
users: state.usersSlice.users
}
}
const mapDispatchToProps= (dispatch) => {
return {
onFetchUsers: () => dispatch(userActions.fetchUsers())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserContainer);
the user actions:
import axios from 'axios';
export const FETCH_USERS = 'FETCH_TODOS';
export const FETCH_USERS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchRequest = () => {
return dispatch => {
dispatch(fetchUsersStarted());
axios.get('https://one365-api-dev.azurewebsites.net/api/me')
.then(result => {
dispatch(fetchUsersSuccess(result));
}).catch(error => {
dispatch(fetchUsersFailure(error));
});
}
}
export const fetchUsersSuccess = (users) => {
return {
type: FETCH_USERS_SUCCESS,
payload: {
...users
}
}
}
export const fetchUsersStarted = () => ({
type: FETCH_USERS_STARTED
});
export const fetchUsersFailure = (error) => {
return {
type: FETCH_USERS_FAILURE,
payload: {
error
}
}
}
export const fetchUsers = () => {
return dispatch => {
dispatch(fetchRequest())
}
};
And it's reducer:
import * as actions from './actions';
const initialState = {
error: '',
loading: false,
users: []
}
const userReducer = (state = initialState, action) => {
switch(action.type) {
case actions.FETCH_USERS_STARTED: {
console.log('fetch users state', state)
return {
...state,
loading: state.loading = true
}
}
case actions.FETCH_USERS_SUCCESS: {
const users = action.payload.data;
return {
...state,
loading: false,
users: state.users = users
}
}
case actions.FETCH_USERS_FAILURE: {
const error = state.payload.error;
return {
...state,
loading: false,
error: state.error = error
}
}
default: {
return state;
}
}
}
export default userReducer;
Now when I run my DEV server I only see the fetch todo button. I commented out the users on click handler to see if it was an event bubble going up. Bu t this wasn't the case.
Once the app load redux dev tools shows the state as follows:
but once i click the fetch todo's handler. Both todos and users get filled.
I appreciate anyone who read though so much (boilerplate) code. I probably made a problem encapsulating my state. but again after reading many tutorials I still cannot find my issue.
You have a copy/paste issue. You changed the names of the constants for your "USERS" actions, but left the values the same as the "TODOS" actions.
export const FETCH_USERS = 'FETCH_TODOS';
export const FETCH_USERS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_TODOS_FAILURE';
I assume you meant to have:
export const FETCH_USERS = 'FETCH_USERS';
export const FETCH_USERS_STARTED = 'FETCH_USERS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
This is my component:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Divider } from "antd";
import MovieList from "../components/MovieList";
import IncreaseCountButton from "../components/IncreaseCountButton";
import DeleteButton from "../components/DeleteButton";
import { deleteMovie, increaseCount } from "../actions/movies";
import { getIsDeleting, getIsIncreasing } from "../reducers/actions";
export class MovieListContainer extends Component {
constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
static propTypes = {
isIncreasing: PropTypes.func.isRequired,
isDeleting: PropTypes.func.isRequired,
};
async handleIncrease(movie) {
await this.props.increaseCount(movie, this.props.token);
}
async handleDelete(movie) {
await this.props.deleteMovie(movie.id, this.props.token);
}
render() {
return (
<MovieList movies={this.props.movies}>
{(text, movie) => (
<div>
<IncreaseCountButton
onIncrease={() => this.handleIncrease(movie)}
loading={this.props.isIncreasing(movie.id)}
/>
<Divider type="vertical" />
<DeleteButton
onDelete={() => this.handleDelete(movie)}
loading={this.props.isDeleting(movie.id)}
/>
</div>
)}
</MovieList>
);
}
}
export const mapStateToProps = state => ({
isIncreasing: id => getIsIncreasing(state, id),
isDeleting: id => getIsDeleting(state, id),
});
export default connect(
mapStateToProps,
{ deleteMovie, increaseCount }
)(MovieListContainer);
I feel like this might be bad for performance/reconciliation reasons, but not sure how else to retrieve the state in a way that hides implementation details.
Gist link: https://gist.github.com/vitalicwow/140c06a52dd9e2e062b2917f5c741727
Any help is appreciated.
Here is how you can handle these asynchronous actions with redux. You can use thunk to perform 2 actions and can store a flag to determine what is being done to an object (Deleting, Changing, etc):
action
export const deleteMovieAction = id => {
return dispatch => {
dispatch({ type: "MOVIE_DELETING", id });
setTimeout(() => {
dispatch({ type: "MOVIE_DELETED", id });
}, 2000);
};
};
reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movies = [...state.movies];
movies.find(x => x.id === action.id).isDeleting = true;
return { ...state, movies };
}
case "MOVIE_DELETED": {
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/k3jnv01ymv
An alternative is to separate out the ids into a new array that are being deleted
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movieDeletingIds = [...state.movieDeletingIds, action.id];
return { ...state, movieDeletingIds };
}
case "MOVIE_DELETED": {
const movieDeletingIds = state.movieDeletingIds.filter(
x => x.id !== action.id
);
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movieDeletingIds, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/mj52w4y3zj
(This code should be cleaned up, but is just to demo using thunk)
I'm using this package https://github.com/RealScout/redux-infinite-scroll to make infinite scroll on list of brand. Here is my code:
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, getBrands } from '../reducer';
import Infinite from 'react-infinite';
import InfiniteScroll from 'redux-infinite-scroll';
import SearchBox from '../components/SearchBox';
import CardList from '../components/CardList';
const { fetchBrands } = actions;
class BrandList extends Component {
componentDidMount() {
this.props.fetchBrands({ page: 1 });
}
renderList() {
const brands = this.props.brands;
return brands.map((brand) => {
return (
<CardList key={brand.id} name={brand.name} avatar={brand.avatar.thumbnail} follower={brand.follows_count} />
);
});
}
toggle() {
return this.props.isFetching;
}
loadMore() {
const {lastPage, currentPage} = this.props;
const nextPage = currentPage ? parseInt(currentPage) + 1 : 1;
if(currentPage && currentPage <= lastPage){
this.props.fetchBrands({page: nextPage});
}
}
render() {
return (
<div>
<SearchBox />
<div className="row">
<InfiniteScroll
items={this.renderList()}
loadMore={this.loadMore.bind(this)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
brands: getBrands(state),
isFetching: state.brand.isFetching,
currentPage: state.brand.currentPage,
lastPage: state.brand.lastPage
};
}
export default connect(mapStateToProps, { fetchBrands })(BrandList);
Reducer:
import axios from 'axios';
// Define Types
export const types = {
// brand list
FETCH_BRANDS: 'fetch_brands',
FETCH_BRANDS_SUCCESS: 'fetch_brands_success',
FETCH_BRANDS_ERROR: 'fetch_brands_failure',
FETCH_BRAND: 'fetch_brand',
FETCH_BRAND_SUCCESS: 'fetch_brand_success',
FETCH_BRAND_ERROR: 'fetch_brand_failure',
};
const { FETCH_BRANDS, FETCH_BRANDS_SUCCESS, FETCH_BRANDS_ERROR } = types;
// Define Reducer
export const INITIAL_STATE = { brands: [], brand: {}, isFetching: false, error: null, currentPage: 1 };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BRANDS:
return { ...state, isFetching: true };
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: action.payload.brands.data, currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
case FETCH_BRANDS_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
// Define Actions
export const actions = {
fetchBrands: ({page, count = 15}) => {
return (dispatch) => {
dispatch({ type: FETCH_BRANDS });
axios.get(`brands?page=${page}&count=${count}`)
.then((response) => {
const {data} = response;
if (data.code == 200) {
dispatch({ type: FETCH_BRANDS_SUCCESS, payload: data });
}
});
};
}
};
// SELECTOR
export const getBrands = (state) => state.brand.brands;
it run loadMore function successfully but it not extend current list, it replace it instead.
loadmore function only run once. it should run 10 times.
do I miss something on my code to make it scroll?
Try adding
brands: [ ...state.brands, ...action.payload.brands.data]
like this in your reducer
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: [ ...state.brands, ...action.payload.brands.data], currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
Which means that you are concating current list with upcoming list (versioned data)