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).
Related
I have a redux action / reducer that looks like the following.
Action:
export function loadServerInfo() {
return (dispatch) => axios.get(`${config.SERVER}/redis/server/info`).then(res => {
if (res.status == 200) {
dispatch(fetchServerInfo(res.data))
}
}).catch(err => {
})
}
export function fetchServerInfo(payload) {
return {
type: GET_SERVER_INFO,
payload
}
}
Reducer:
const defaultState = {
decodedRedisKey: {},
keyDecoded: false,
serverInfo: {}
}
const redisReducer = (state = defaultState, action: Action) => {
switch (action.type) {
case GET_REDIS_KEY_INFO: {
return {
...state,
decodedRedisKey: action.payload
}
}
case REDIS_KEY_DECODED: {
return {
...state,
keyDecoded: action.payload
}
}
case GET_SERVER_INFO: {
console.log(action.payload) //this is fired and logs the proper data, which is an object
return {
...state,
serverInfo: action.payload
}
}
default:
return {
...state
};
}
}
export default redisReducer;
Then I have a component connected and mapped to redux. Those are the connection parameters
const mapStateToProps = (state) => state;
function mapDispatchToProps(dispatch) {
return {
loadServerInfo: async () => {
dispatch(loadServerInfo());
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UsersContainer);
And after that, I try to call the fetch, and get the data.
Problem is that the format of the object is as follows:
serverInfo: {
Server : {
uptime_in_days: "100",
version: "1.0.0"
}
}
My prop is firing on useEffect
React.useEffect(() => {
getUsersToken();
props.loadServerInfo();
console.log(process.env.REACT_APP_ENV)
}, []);
If i put it in a useEffect, first it logs undefined and afterward it loads
React.useEffect(() => {
console.log("server info")
console.log(props.redisReducer.serverInfo)
console.log(props.redisReducer.serverInfo.Server)
// console.log(props.redisReducer.serverInfo.Server.uptime_in_days) , if i uncomment this it crashes
}, [props.redisReducer.serverInfo])
So im having issues rendering the uptime_in_days value
I have tried doing this
{props.redisReducer.serverInfo != undefined && !displayServerInfo != undefined ?
<div className="basic-server-info-data">
<p><img src={redisLogo} /></p>
{/* <p>Connected Clients: <i>{serverInfo.Clients.connected_clients} </i></p> */}
{/* <p>Memory usage: <Progress type="circle" percent={memoryUsageStats} width={50} /> </p> */}
<p>Tokens (displayed): <i>{usersToken.length}</i></p>
<p>Uptime: <i>{props.redisReducer.serverInfo.Server.uptime_in_days} days</i></p>
</div>
:
null
}
It keeps crashing in the Uptime line, even tho im doing a check if its not undefined
Cannot read property 'uptime_in_days' of undefined
I tried changing the render condition to
props.redisReducer.serverInfo != undefined && !displayServerInfo != undefined && props.redisReducer.serverInfo.Server.uptime_in_days != undefined
But nothing changes.
How can I render that value?
EDIT: I have noticed this error
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
in my useEffect
Issue
The issue is that all your null checks start with the always defined state, props.redisReducer.serverInfo
const defaultState = {
decodedRedisKey: {},
keyDecoded: false,
serverInfo: {} // <-- defined!
}
state.serverInfo is always a defined object, so console.log(props.redisReducer.serverInfo) and console.log(props.redisReducer.serverInfo.Server) will always log, and the condition props.redisReducer.serverInfo != undefined will always be true.
You neglect to do a null check on props.redisReducer.serverInfo.Server before accessing the uptime value
props.redisReducer.serverInfo.Server.uptime_in_days
I'm guessing your UI is blowing up on the initial render before state is populated.
Solutions
Use Optional Chaining to handle the null check on Server being possibly undefined still.
props.redisReducer.serverInfo.Server?.uptime_in_days
Use conventional null checks
props.redisReducer.serverInfo.Server &&
props.redisReducer.serverInfo.Server.uptime_in_days
This is my first time using this platform to ask questions so please pardon me if my question does not seem well developed.
brief introduction
what I am trying to achieve is a dynamic Tab navigator, whereby the number of tabs changes depending on the number of elements in an array where this array changes in the number of elements over time, i.e :
{
userIds : [1,2,3,4,5,6]
}
will render a tab navigator with 6 tabs
I am using react-redux for managing state and I have been following this tutorial on youtube just for your information: https://www.youtube.com/watch?v=9boMnm5X9ak&list=PLC3y8-rFHvwheJHvseC3I0HuYI2f46oAK
context
in the main code snippet the action FetchMonthlyTransIdAct() is being dispatched, this consist of 2 actions being dispatched in order :
RequestMonthlyTransaction → FetchSuccess or FetchFail
(as per mentioned in FetchMonthlyTransIdAct.js, ) the initial state is as follows and the changes each action does :
{
loading : false
Id : []
error : ''
}
{
loading : true //when RequestMonthlyTransaction is dispatched
Id : []
error : ''
}
{
loading : false // When FetchSuccess is dispatched after RequestMonthlyTransaction
Id : [1,2,3,4,5,6]// When FetchSuccess is dispacthed after RequestMonthlyTransaction
error : ''
}
{
loading : false //when FetchFail is dispacthed after RequestMonthlyTransaction
Id : []
error : 'some error message here' //when FetchFail is dispatched after RequestMonthlyTransaction
}
problem
so the problem that I am currently facing is that useEffect does not seem to trigger when I am rendering components with navigationContainer/ tab.navigator
here is the snippet of my code, I have narrowed down the source of the problem between asterisks
const Tab = createMaterialTopTabNavigator();
const mapStateToProps = state => {
return {
userData: state.MonthlyEntry
}
}
const mapDispatchToProps = dispatch => {
return {
FetchMonthlyTransId: () => dispatch(FetchMonthlyTransIdAct())
}
}
const EntryTabNavigator = ({userData, FetchMonthlyTransId}) => {
useEffect (() => {
FetchMonthlyTransId()
}, [])
console.log(userData.Id)
if (userData.loading || userData.error != '') {
return <View/>
} else {
return(
**************************************************************************************
<NavigationContainer independent = {true}>
<Tab.Navigator swipeEnabled = {true} tabBarOptions = {{scrollEnabled:true, tabStyle:{width:120}}}>
{userData.Id.map((data) => {return (<Tab.Screen key = {data.toString()} name = {data.toString()} component = {MonthlyTransactions} initialParams={{id:data.toString()}}/>)})}
</Tab.Navigator>
</NavigationContainer>
**************************************************************************************
)
}
};
export default connect(mapStateToProps, mapDispatchToProps)(EntryTabNaviga
the error message simply that there was no screen for tab navigator to render (due to userData.Id being an empty array when it should not)
based on the console.log(userData.Id)
the expected output should be Array [1,2,3,4,5,6]
but the actual output was Array [] which indicates that the useEffect was not triggered
I have tried replacing the snippet of code between the astericks with
<View><Text>{userData.Id}</Text><View> and it was able to render as expected (returning a screen with the string representation of the array as the text), hence leading me to identify that the code snippet between the astericks is the problematic portion. I have also tried adding a console.log statement within useEffect and it does not output anything into the console when I have the code snippet in asterisks, however it does output into the console when I replaced the snippet of code between the astericks with <View><Text>{userData.Id}</Text><View>
should there be a similar problem to this that has already been asnwered, it would be much apppreciated if you could direct me to it, it would also be great if you could point me to resources to improve my knowledge with redux (prefreably beginner friendly) ! additional reference code (reducer and action) is below
Thank you in advance
FetchMonthlyTransIdAct.js
const requestMonthlyTransaction = () => {
return {
type: "REQUEST_MONTHLY_TRANSACTION",
}
}
const fetchSucess = (ids) => {
return {
type: "FETCH_SUCCESS",
payload: ids,
}
}
const fetchFail = (error) => {
return {
type: "FETCH_FAILURE",
payload: error,
}
}
export const FetchMonthlyTransIdAct = () => {
return (dispatch) => {
dispatch(requestMonthlyTransaction())
async function getId() {
return require('../../data/DummyId.js').id //returns [1,2,3,4,5,6]
}
getId().then(
id => dispatch(fetchSucess(id))
).catch(
error => dispatch(fetchFail(error))
)
}
}
FetchMonthlyTransIdRed.js
const initialState = {
loading:false,
Id : [],
error:''
}
const FetchMonthlyTransactionIdRed = (state = initialState, action) => {
switch (action.type){
case "REQUEST_MONTHLY_TRANSACTION":
return {
...state,
loading: true,
}
case "FETCH_SUCCESS":
return {
...state,
loading: false,
Id: action.payload
}
case "FETCH_FAILURE":
return {
...state,
loading: false,
error: action.payload
}
default: return state;
}
}
export default FetchMonthlyTransactionIdRed;
after much tinkering, I manage to find a solution (or a workaround rather) to the problem. which is to add an initial element in the array of the Id attribute in the initial state in FetchMonthlyTransIdRed.js, that will allow the first render of the navigation component to occur without issues, and subsequently in the next re-render when FetchMonthlyTransId is dispatched Id is then updated with the array that I have imported
Use React Navigation's useFocusEffect, e.g.:
import { useFocusEffect } from '#react-navigation/native';
function Profile({ userId }) {
const [user, setUser] = React.useState(null);
useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, user => setUser(user));
return () => unsubscribe();
}, [userId])
);
return <ProfileContent user={user} />;
}
I have three different components in my app.
ie. 1. Newsfeed (here i am fetching all the posts)
2. Trending (posts with of popular hashtags)
3. UserProfile (fetching all the posts of that user)
const postReducer = (state = postReducerDefaultState, action) => {
switch(action.type){
case 'ADD_POST':{
return[
action.post,
...state
]
}
case 'FIND_POST':
return action.post
case 'REMOVE_POST':
return state.filter(post => post._id !== action.id)
case 'EDIT_POST':
return state.map(post => {
if (post._id === action.id){
return{
...post,
...action.updates
};
}else{
return post;
}
});
case 'LIKE_POST':
return state.map(post => {
if(post._id === action.id){
return{
...post,
likes:[...post.likes,action.likedBy]
}
}else{
return post;
}
});
case 'UNLIKE_POST':
return state.map(post => {
if(post._id === action.id){
return {
...post,
likes : post.likes.filter(liker => liker !==
action.likedBy)
}
}else{
return post;
}
})
case 'ADD_COMMENT':
return state.map(post => {
if(post._id === action.pid){
return{
...post,
comments:[...post.comments,action.comment]
}
}else{
return post;
}
});
case 'SET_POST':
return action.posts;
case 'HASHTAG_POST':
return action.posts;
default:
return state;
}
};
in this design if i go from one component to other component, (dashboard to trending), I lost all the data of the dashboard. But if want to keep the like,comment functionalty for dashboard and trending posts, then i have to write the like-comment functionality separately for all the mentioned three components. This approach will work, but i am not satisfied with as i have to write same thing thrice. Can anyone suggest the best approach?
The recommended way to solve this problem is with higher order components, which are React components that "wrap" another component to provide common functionality. You should be able to find plenty of resources explaining this, but a typical higher order component looks something like this:
import React from 'react';
const higherOrderComponent = (WrappedComponent) =>
{
class HOC extends React.Component {
render() {
return <WrappedComponent />;
}
}
return HOC;
};
You can then add whatever functionality needs to be shared to the higher order component. Then, you can use it something like this:
const SimpleHOC = higherOrderComponent(MyComponent);
EDIT: If you want the same functionality to apply for different reducer actions, that's straightforward. Just have the same callback fall through for different cases, as in this example:
case 'ACTION_1':
case 'ACTION_2':
case 'ACTION_3':
// callback here
For your application, it will be something like this:
case 'EDIT_POST':
case 'LIKE_POST':
case 'UNLIKE_POST':
return state.map(post => {
if (post._id === action.id){
return{
...post,
...action.updates
};
}else{
return post;
}
});
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.
}
}
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.