Background
I am working on my first project using Redux. I am trying to sort data in a reducer. The reducer has 2 sort methods. One for sorting data based on the timeStamp and one by the VoteScore.
Problem
The reducer fires off, I can console.log() the data and see it is being passed to the correct case when sort is fired off, but the data does not sort on the screen.
**
I can see when I console.log the data that the data is sorting. So it
is just not rerendering.
**
Example of Data
(2) [{…}, {…}]
{timestamp: 1468479767190, voteScore: -5, …}
{timestamp: 1467166872634, voteScore: 6, …}
Example Reducer
function posts(state = [], action) {
switch(action.type) {
case 'SORT_POSTS': {
switch (action.attribute) {
case 'BY_TIMESTAMP':
return state.sort(function(a, b) {
return a.timestamp - b.timestamp;
});
case 'BY_VOTESCORE':
return state.sort(function(a, b) {
return a.voteScore - b.voteScore;
});
default:
return state;
}
}
case SET_POSTS:
return action.posts;
default:
return state;
}
}
Example Dispatch & onClick Method
<button onClick={() => this.props.boundSortPosts(this.state.timeStamp)}>Click MeK</button>
const mapDispatchToProps = (dispatch) => ({
boundSortPosts: (attribute) => dispatch(sortPosts(attribute))
});
Example Action
export function setPosts(posts) {
return {
type: SET_POSTS,
posts,
}
}
export function sortPosts(attribute) {
return {
type: SORT_POSTS,
attribute,
}
}
Initially Rendering Data Like This
render() {
return (
<div>
<Posts />
<div><button onClick={() => this.props.boundSortPosts(this.state.timeStamp)}>Sort by Time Stamp</button></div>
<div><button onClick={() => this.props.boundSortPosts(this.state.voteScore)}>Sort by Vote Score</button></div>
{
this.props.posts.map((post, index) => {
return (
<div key={index}>
<h4>{post.timestamp}</h4>
<h4>{post.voteScore}</h4>
</div>
)
})
}
</div>
)
}
Question
What must I do to cause a rerender at this point?
First and very important thing is you should not mutate your state. The sort() method sorts the elements of an array in place and returns the array.You can create a copy of the state array by calling state.slice() before sorting in your reducer, something like this :
return state.slice().sort(function(a, b) {
return a.timestamp - b.timestamp;
});
Regarding your component not re-rendered, please check your mapStateToProps. It should be something like this:
function mapStateToProps(state} {
return {
posts: state.posts // posts is reducer which is passed to your store.
}
}
Related
So me and my colleague are writing a React/ServiceNow project for internal use of our company.
I'm using the reducer hook to manage state throughout the app. I've got the following structure App components, which calls the API (array with questions and their respective answers). This info is passed down to TrainingMode and that returns
return
(
<QuizContext.Provider value={{ state, dispatch }}>
<div className='container'>
<Progress />
{'Training'}
<Question />
{renderError()}
<Answers />
<button className='btn btn-primary' onClick={next}>
Confirm and Continue
</button>
</div>
</QuizContext.Provider>
);
Answers:
function Answers() {
const { state, dispatch } = useContext(QuizContext);
const { currentAnswer, currentQuestion, questions } = state;
const question = questions[currentQuestion].question;
const answers = questions[currentQuestion].answers;
console.log('curr ans is ' + question);
console.log(answers);
let firstLetter = 65;
const ans = answers.map((el) => {
return (
<Answer
key={el.sys_id}
letter={(firstLetter++).toString()}
answer={el.answer_text}
answer_sysId={el.sys_id}
selected={currentAnswer === el.sys_id}
dispatch={dispatch}
/>
);
});
return <>{ans}</>;
Individual Answer:
function Answer(props) {
let classes = ['answer'];
const handleClick = (e) => {
props.dispatch({
type: SET_CURRENT_ANSWER,
currentAnswer: props.answer_sysId
});
props.dispatch({ type: SET_ERROR, error: '' });
};
if (props.selected) {
classes.push('selected');
}
return (
<button
value={props.answer}
className={classes.join(' ')}
onClick={handleClick}
>
<span>{String.fromCharCode(props.letter)}.</span> {props.answer}
</button>
);
}
And last, the reducer:
function quizReducer(state, action) {
switch (action.type) {
case SET_CURRENT_ANSWER:
console.log(action);
state.currentAnswer.push(action.currentAnswer);
return {
...state
// currentAnswer: action.currentAnswer
};
case SET_CURRENT_QUESTION:
return {
...state,
currentQuestion: action.currentQuestion
};
case SET_ERROR:
return {
...state,
error: action.error
};
case SET_SHOW_RESULTS:
return {
...state,
showResults: action.showResults
};
case SET_ANSWERS:
return {
...state,
answers: action.answers
};
case ADD_CORRECT:
return {
...state,
correct: action.correct
};
case RESET_QUIZ:
return {
...state,
answers: [],
currentQuestion: 0,
currentAnswer: '',
showResults: false,
error: '',
correct: 0
};
default:
return state;
}
I want to click on each individual answer and push it to the currentAnswer array (so it works for both single and multiple choice questions). It kind of works, it pushes the first answer I click just once, however when I click on other answers, it pushes them twice. When I comment out the Strict mode tag, everything works fine, however I doubt that's the best solution.
console log
I've read though some articles but can't seem to fix this.
Ciao, you know that when a component is rendered, react triggers useEffect hook. So you could try to put you logs in this hook. Something like:
import { useEffect } from 'react';
...
useEffect(() => {
console.log('curr ans is ' + question);
console.log(answers);
})
If the logs appears twice, then the Answer component is rendered twice. Otherwise not.
So now a question: why you see doubled logs? My answer is I don't know. Could be related on react workflow but this is just my opinion (I'm not soo expert in react).
I faced a problem with React Redux when I need for one page render multiple lists of items from the same API. For example, I have endpoint /items with GET param /items?city=Berlin. I need to render 5 cities lists of items on the frontpage.
Currently I have 1 action to handle API call.
Here is my reducer.js:
...
const initialState = fromJS({
...
items: [],
...
})
export default function reducer (state = initialState, action) {
...
switch (action.type) {
case fetchItemsByCityRoutine.SUCCESS:
return state.update('items', (items) => items.push(...action.payload.data))
...
}
}
saga.js:
function * fetchItemsByCity ({ payload: city }) {
try {
yield put(fetchItemsByCityRoutine.request())
const response = yield call(apiClient.fetchItemsByCity, city)
response.city = city
yield put(fetchItemsByCityRoutine.success(response))
} catch (error) {
yield put(fetchItemsByCityRoutine.failure(extractErrorMessage(error)))
} finally {
yield put(fetchItemsByCityRoutine.fulfill())
}
}
function * watchItemsByCitySaga () {
yield takeEvery(fetchItemsByCityRoutine.TRIGGER, fetchItemsByCity)
}
On the Homepage I render list of cities like this:
const renderCityListSections = () => homepageCityLists.map((city) => <ItemList key={city.cityName} {...city} />)
ItemList component:
class ItemList extends Component {
componentDidMount () {
const { cityName } = this.props
this.props.fetchItemsByCityRoutine(cityName)
}
render () {
const { items, title } = this.props
return (
items.length > 0 &&
<Wrapper>
<SlickSlider>
{items.map((item) => <Item key={item.id} {...item} />)}
</SlickSlider>
</Wrapper>
)
}
}
THE PROBLEM is that current solution makes rerender view too many times because every time I fetch new list for some city it adds to items, so items changes and it leads to trigger rerendering view.
I had a thought to create 5 different Actions for every city, but it's not seems to be reasonable solution.
WHAT is the best approach to render multiple lists of cities on one page?
UPDATE:
So I changed reducer to look like this:
const initialState = fromJS({
...
items: {
Miami: [],
Prague: [],
Melbourne: [],
Venice: [],
Barcelona: [],
},
...
})
export default function reducer (state = initialState, action) {
switch (action.type) {
...
case fetchItemsByCityRoutine.SUCCESS:
return state.updateIn(['items', action.payload.city], (list) => (
list.concat(action.payload.data)
))
...
}
}
so every array is immutable, but again it runs into too much rerendering.
Everybody who has such a problem I ended up with this SOLUTION:
In my reducer I decoupled every item for city like this:
const initialState = fromJS({
...
itemsForBerlin: [],
itemsForAnotherCity: [],
...
})
export default function reducer (state = initialState, action) {
switch (action.type) {
...
case fetchItemsByCityRoutine.SUCCESS:
return state.set(`itemsFor${action.payload.city}`, action.payload.data)
...
}
}
I am a beginner in react.js. I am making a board game as a beginner project. There is a component which rolls the dice. When the dice is rolled, an action is dispatched so that the state is changed to reflect values of that rolled dice. But in the reducer, both the values of state and action.data are same. How is that possible?
I have a dice rolling component which return this:
return (
<div>
<button
onClick={this.rollDice.bind(this)}
>
Roll dice!
</button>
<TurnDisplay dispatch={this.dispatch} />
<BoardContainer dispatch={this.dispatch} />
</div>
);
When you click the button, an action is dispatched, which is supposed update the state by adding dice values in it.
rollDice(){
console.log("this.props ||||| rollDice()", this.props);
if(this.props.isDiceRolled){
alert("Move the marker...");
return;
}
this.props.dispatch({
type: "ROLL_DICE",
data: {
dice: Math.ceil(Math.random() * 5),
isDiceRolled: true
}
});
}
In reducer:
switch(action.type){
.
.
.
case "ROLL_DICE": {
console.log("ROLL_DICE");
console.log(state, action.data);
const newTurn = Object.assign(state.currentTurn, action.data);
return Object.assign(state, { currentTurn: newTurn });
}
default: {
return state;
}
}
With Redux, in the reducers, every time you have to return a new state.
So instead of these two lines:
const newTurn = Object.assign(state.currentTurn, action.data);
return Object.assign(state, { currentTurn: newTurn });
You should write something like this
const newTurn = Object.assign({}, state.currentTurn, action.data);
return Object.assign(
{},
state,
{ currentTurn: newTurn}
)
The problem should be with Object.assign. Read more here: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
I am trying to simply sort on the redux store data in mapStateToProps, similar to how it is being done in Dan Abramov's Egghead.io video: https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers
My problem is, initially the state is returning undefined (as it is fetched asynchronously), so what would be the best way to deal with this? Current code is as follows (_ is the ramda library):
const diff = (a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
}
const mapStateToProps = (state) => {
return {
transactions: _.sort(diff, state.transactions.all),
expenditure: state.expenditure.all,
income: state.income.all
}
}
I thought that transactions.all should initially be an empty array (which would mean the code would work) because of the initial state set in the reducer:
const INITIAL_STATE = { transactions: { all: [] }, transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_TRANSACTION:
return { ...state, transaction: action.payload.data }
case FETCH_TRANSACTIONS:
return { ...state, all: action.payload.data }
case EDIT_TRANSACTION:
return { data: action.data }
case ADD_TRANSACTION:
return { data: action.data }
case DELETE_TRANSACTION:
return { ...state }
default:
return state
}
}
Thanks in advance.
As you said, it is fetched asynchronously. Perhaps when the component rendered, data isn't ready yet which resulted to an undefined object.
const SampleComponent = (props) => {
if(props.transaction === undefined)
return <Spinner /> // Loading state
else
// your implementation
}
You can further make the code cleaner as explained by Dan himself in the docs here: http://redux.js.org/docs/advanced/AsyncActions.html
Managed to solve this, because in combine reducers, I had set transactions with the name transactions and then in the reducer, I essentially had the initial state set to transactions: { all: [] } }.
This was causing state.transactions.all to be undefined, as the correct state structure was actually state.transactions.transactions.all.
After updating the transactions reducer to:
const INITIAL_STATE = { all: [], transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {...
The initial empty transactions array prior to the promise returning meant the sort no longer causes an error, and is then correctly sorted on load.
I know there are a lot of similar questions, but I was unable to find the right answer sifting through the others. The issue seems to be that {loopToDo} does not directly reference a prop from the store. How can I set my code up so that it updates when the store changes, like I want it to?
#connect((germzFirstStore) => {
return {
taskList: germzFirstStore.tasks
}
})
class TaskBoard extends React.Component {
render() {
function toDoStatus(value) {
return value.taskstatus === "toDo";
}
var toDoTasks = this.props.taskList.tasks.filter(toDoStatus);
var loopToDo = toDoTasks.map((tasksEntered) => {
return (
<div id={tasksEntered.idtasks} className="taskBox">{tasksEntered.task}</div>
);
});
return(
<div ref="toDo" id="toDo" className="container toDo">{loopToDo}</div>
)
}
}
the reducer:
const tasksReducer = (state=tasksInitialState, action) => {
if (action.type === "ADD") {
state = {...state, tasks: [...state.tasks, action.newTask]}
}
return state; }
The problem here is that by doing this
state = {...state, tasks: [...state.tasks, action.newTask]}
You are effectively mutating the state before returning it and that probably is the reason why your components are not re-rendering on updating state.
What you can do in your reducer is
if (action.type === "ADD") {
return {...state, tasks: [...state.tasks, action.newTask]}
}
or
if (action.type === "ADD") {
return Object.assign({}, state, {tasks: [...state.tasks, action.newTask]})
}
Hope it helps :)
If germzFirstStore.tasks has updated then the component will re-render it has nothing to do with what is inside the render function. My guess is that you are mutating the state in your reducer instead of updating it and returning the updated version of the state.