...inside a dispatch, in the path: "chat.messages.0' Take a look at the reducer(s) handling the action {'type': 'chat', 'payload': {'sender': 'you', 'body': 'foo'}}`
My reducer:
chatMessage.js:
export default (state, action) => {
return Object.assign({}, {
...state,
messages: [...state.messages, action.payload]
})
}
Interestingly, if I use messages=[] in my Chat component, I don't get this error.
export default class Chat extends React.Component {
state = {...this.props.data, input: "", toggle: false}
_renderItem = ({item}) => {
return <ChatMessageRow data={item} />
}
render() {
// var {messages} = this.state
var messages = []
return (
<View style={styles.container}>
<FlatList
inverted
renderItem={this._renderItem}
style={styles.logWrapper}
data={messages.reverse()}
extraData={this.state.toggle}
/>
from my main view, which is bound to Redux:
return (...other views... check if tabIndex == 1, show <Chat data={this.props.chat} ... />
I ran into an issue earlier with the FlatList rendering in Chat not updating and I had to do a
this.setState({messages: messages.push(newMessage), toggle: !this.state.toggle})
to update the state so the Flat List would recognize it's changed.
Now I am loading my data from a Chat store using connect and redux, then passing that data into the component.
I get the error when I try to reload the Chat component after adding chat messages. Weirdly I can get one to update but after adding one it will not show others.
At first I think this is a problem in my reducer so I rewrite the reducer to use the Object assign and array spread operator.
Now I think it is related to the rendering which is unexpected.
How do I debug this?
EDIT:
It's not "it works with adding 1 message". It's - I can bring the view out of focus and back into focus one time. So..
Open Chat Tab
Observer 1 message
Add N messages to store using redux actions
Change tabs, revert back to Chat
Observer N messages added
Add M messages
Change tabs, click back to Chat tab
Error shows
EDIT:
I tried
using ...spread, but redux still throws warning about state mutation 's suggestion doing
export default (state, action) => {
return Object.assign({}, {
...state,
messages: state.messages.map(value => Object.assign({}, value)).concat(action.payload)
})
}
in my reducer, same error.
EDIT:
My reducer is update to chatMessage.js
I think the issue is in how I'm calling this.
I am writing a websocket controller. I don't need to wait for a response when I send a message like I would with an HTTP response.
My websocket controller:
onMessage = ({data}) => {
const json = JSON.parse(data)
if (json) {
if (json.status) {
const reducer = this.stateFilters[json.status]
if (reducer) {
reducer(json.body)
} else {
console.log("No reducer")
}
}
}
}
the websocket controller is created in my View component:
my main view
import {ChatStateFilters} from '../../reducers/chat'
const mapDispatch = { chatMessage }
this.wsController = new WebSocketController({
stateFilters: {chatMessage: this.props.chatMessage},
from the chat reducer file (that contains the chatMessage reducer)
Those state filters get created when I create my slice.
The value of stateFilter['chatMessage'] -- which is the value of what gets bound to my View's prop in mapDispatchToProp
is this function:
let fn = (payload) => {
return dispatch => {
dispatch(stateFilters[actionName](payload))
}
}
exportedStateFilters[actionName] = fn
I think the problem is somewhere in here^... somehow the dispatch is firing, updating state but redux doesn't know the dispatch is finished
EDIT 2:
I thought the lack of async may have been an issue. So I changed the stateFilter (the fn that gets bound to my prop in the view) to this:
let fn = (payload) => {
return dispatch => {
post("", {}, true)
.then(response => {
dispatch(stateFilters[actionName](payload))
})
.catch(error => {
dispatch(stateFilters[actionName](payload))
})
}
}
and now it works a few more times.. then gives me the error "A state mutation was detected between dispatches".
My chat message reducer is still
export default (state, action) => {
let messages2 = Object.assign([], [...state.messages, action.payload])
return Object.assign({}, {...state, messages: messages2 })
}
so why is this error happening?
Related
I got 3 pages
homepage, productList and productDetails
When going from homepage to productList I pass a route param,
navigation.navigate('productList', { showCategory: 'productListA'} )
InitialProcess when component mounted
Inside the productList page when the component is mounted. I am declaring use state like this.
const {showCateory} = route.params;
const [activeTab, setActiveTab] = useState(showCateory);
and calling api using that activeTab
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
User Interaction
But I also add the button in the productList so that user can change the current active tab
<TouchableOpacity onPress={() => changeTab()}></TouchableOpacity>
const changeTab = async () => {
await setActiveTab('productListB'),
await dispatch(fetchProductList(activeTab)
}
Take note that right now active tab and data coming from api is different from when the component is start mounted.
Navigation Change again
When use goes from productList to productDetails. All thing is fine.
But inside the product details I am going back to productList with this.
navigation.goBack().
When I am back in productList page The activeTab is change back to productListA and the data is change back to when component is mounted
Can I pass or change the route params when calling navigation.goBack()?
add activeTab in useEffect depedineces.
as docs say
The array of dependencies is not passed as arguments to the effect function. Conceptually, though, that’s what they represent: every value referenced inside the effect function should also appear in the dependencies array. In the future, a sufficiently advanced compiler could create this array automatically.
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
//this value will always updated when activeTab change
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, [activeTab]); //<<<<< here
also you need to know setState() does not always immediately update the component. see here
so change this
const changeTab = async () => {
//await setActiveTab('productListB'),
//await dispatch(fetchProductList(activeTab)
setActiveTab('productListB')
dispatch(fetchProductList('productListB'))
}
This might be happening because route.params is still set to { showCategory: 'productListA'} when you are coming back to the screen.
If this is the case, you can fix it by Changing params object in changeTab() like
navigation.setParams({
showCategory: 'productListB',
});
I hope this will fix your problem.
This happens because the callback function inside the focus listener uses the initial value of the state when the function was defined (at initial page render) . Throughout the lifespan of listener the callback function uses this stale state value.You can read more about this behaviour in this answer
Although the answer by Ahmed Gaber works in this case as the listener is cleared and redefined after each state change.Another common work-around is to use an useRef instead of useEffect.A ref is basically a recipe that provides a mutable object that can be passed by reference.
In your case you can initialise activeTab with navigation param value using useRef hook as :
const activeTab = useRef(showCateory);
and the focus listener callback function should be changed to use the Reference current value as
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab.current, //<<<<<<---------here
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
and the changeTab function can directly update reference current value
const changeTab = async () => {
setActiveTab.current = 'productListB';
dispatch(fetchProductList('productListB'))
}
i have a useEffect function where a redux action is called and data is written to prop. My Problem is that useEffect loop many times and flooded the server with requests.
const { loescherData, navigation } = props;
useEffect(() => {
AsyncStorage.getItem('userdata').then((userdata) => {
if (userdata) {
console.log(new Date());
console.log(userdata);
var user = JSON.parse(userdata);
props.fetchLoescherDetails(user.standort);
setData(props.loescherData);
}
});
}, [loescherData]);
if i leave it blank the rendering is finished before receiving data and the content would not updated.
is there another way to work with this function?
loescherData won't be available right after calling your redux-action fetchLoescherDetails ... and changing component by setData will cause an infinite rendering cause your current useEffect has a dependency on loescherData
So I'd suggest you exec your redux-action onComponentDidMount by passing an empty-deps [] to your effect ... and then consume the output of you action in a different effect
useEffect(() => {
AsyncStorage.getItem('userdata').then((userdata) => {
if (userdata) {
console.log(new Date());
console.log(userdata);
var user = JSON.parse(userdata);
props.fetchLoescherDetails(user.standort);
// setData(props.loescherData);
}
});
}, []);
useEffect(() => {
if (loescherData) {
// do some with loescherData like setState
}
}, [loescherData]);
I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);
I've a button that sends two actions. First one adds the user infos in an array if certain condition is met and 2nd one sends the data to the server.
Since both actions are in onPress function, the 2nd action doesn't wait till it adds up the infos in an array. Henceforth, it always sends empty array.
How can I make this two actions work simultaneously.
<TouchableOpacity
onPress={() => {
if (true) {
this.props.AuthUserInfoGet(SignUpName, SignUpDesignation, SignUpEmail, SignUpMobileNo); //calculates & return SignUpUsers
}
this.props.SignUpCheck(SignUpUsers); //upload SignUpUsers but SignUpCheck is always empty here
}}
>
<Text>Upload</Text>
</TouchableOpacity>
const mapStateToProps = (state) => {
const {SignUpUsers} = state.Auth;
//it gives an empty array first and then expected value
console.log('SignUpUsersz', SignUpUsers);
return {SignUpUsers};
};
Action:
export const AuthUserInfoGet = (SignUpName, SignUpDesignation, SignUpEmail, SignUpMobileNo) => {
return ({
type: SIGN_UP_USER_INFO_GET,
payloadName: SignUpName,
payloadDesignation: SignUpDesignation,
payloadEmail: SignUpEmail,
payloadMobile: SignUpMobileNo,
});
}
export const SignUpCheck = (userInfo) => {
console.log('userInfo', userInfo); // userInfo is always empty
}
Reducer:
const INITIAL_STATE = { SignUpUsers: [] }
case SIGN_UP_USER_INFO_GET:
return { ...state, SignUpUsers: [...state.SignUpUsers, {member_name: actions.payloadName, designation: actions.payloadDesignation,
email: actions.payloadEmail, mobile_number: actions.payloadMobile}] };
Given your current Redux-structure, I think what makes the most sense to use the componentDidUpdate life-cycle method.
The main reason is because your component ultimately needs to get updated data from Redux via props and needs to re-render. When you execute the first action, that user-data coming from the API is not immediately available in the current call-stack, so you'll always be passing an empty array (given your initial value of SignUpUsers: [])
Note that most React-Redux flows follow this path:
User-Event -> Action-Creator -> API (Data) -> Redux -> Component
Your click-event is at step 1 and triggers this action: this.props.AuthUserInfoGet(...args)
But React/Redux needs to go through that entire flow before you can use the new data.
This is where the componentDidUpdate() event comes in-handy because you can write logic when the component is re-rendered by new props or state.
Something like this would totally work:
componentDidUpdate(prevProps){
if(prevProps.SignUpUsers.length !== this.props.SignUpUsers.length){
//execute action
this.props.SignUpCheck(this.props.SignUpUsers)
}
}
For that I would suggest you take a look at redux-thunk middleware.
Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.
And based on your example, the code will end up like this:
<TouchableOpacity
onPress={() => this.props.uploadSignUpUsers(SignUpName, SignUpDesignation, SignUpEmail, SignUpMobileNo)}>
<Text>Upload</Text>
</TouchableOpacity>
const mapStateToProps = (state) => {
const { Auth: { SignUpUsers } } = state;
return { SignUpUsers };
}
Actions:
export const SIGN_UP_GET_USER_INFO_SUCCESS = "SIGN_UP_GET_USER_INFO_SUCCESS";
export const SIGN_UP_UPLOAD_SUCCESS = "SIGN_UP_UPLOAD_SUCCESS";
export const uploadSignUpUsers = (SignUpName, SignUpDesignation, SignUpEmail, SignUpMobileNo) => {
return async (dispatch, getState) => {
// here you can make the api call or any other async calculations
const { data: AuthUserInfo, error } = await api.post(SignUpName, SignUpDesignation, SignUpEmail, SignUpMobileNo);
dispatch({
type: SIGN_UP_GET_USER_INFO_SUCCESS,
payloadName: AuthUserInfo.SignUpName,
payloadDesignation: AuthUserInfo.SignUpDesignation,
payloadEmail: AuthUserInfo.SignUpEmail,
payloadMobile: AuthUserInfo.SignUpMobileNo,
});
const { Auth: { SignUpUsers } } = getState()
// and now you can upload your SignUpUsers
const { data: uploadData, error } = await.api.post(SignUpUsers)
dispatch({
type: SIGN_UP_UPLOAD_SUCCESS,
...uploadData // spread upload data to make it available in reducers
});
}
}
Reducer:
const INITIAL_STATE = { SignUpUsers: [] }
case SIGN_UP_GET_USER_INFO_SUCCESS: {
const { payloadName, payloadDesignation, payloadEmail, payloadMobile } = actions
return {
...state,
SignUpUsers: [ ...state.SignUpUsers, {
member_name: payloadName,
designation: payloadDesignation,
email: payloadEmail,
mobile_number: payloadMobile
}]
}
}
I have a basic component that calls a webservice during the componentDidMount phase and overwrites the contents value in my state:
import React, {Component} from 'react';
import {Text} from "react-native";
class Widget extends Component {
constructor() {
super();
this.state = {
contents: 'Loading...'
}
}
async componentDidMount() {
this.setState(...this.state, {
contents: await this.getSomeContent()
});
}
render() {
return (
<Text>{this.state.contents}</Text>
)
}
async getSomeContent() {
try {
return await (await fetch("http://someurl.com")).text()
} catch (error) {
return "There was an error";
}
}
}
export default Widget;
I would like to use Jest snapshots to capture the state of my component in each one of the following scenarios:
Loading
Success
Error
The problem is that I have to introduce flaky pausing to validate the state of the component.
For example, to see the success state, you must place a small pause after rendering the component to give the setState method a chance to catch up:
test('loading state', async () => {
fetchMock.get('*', 'Some Content');
let widget = renderer.create(<Widget />);
// --- Pause Here ---
await new Promise(resolve => setTimeout(resolve, 100));
expect(widget.toJSON()).toMatchSnapshot();
});
I'm looking for the best way to overcome the asynchronicity in my test cases so that I can properly validate the snapshot of each state.
If you move the asynchronous call out of setState, you can delay setState until the network call has resolved. Then you can use setState's optional callback (which fires after the state change) to capture the state.
So, something like this:
async componentDidMount() {
var result = await this.getSomeContent()
this.setState(...this.state, {
contents: result
},
// setState callback- fires when state changes are complete.
()=>expect(this.toJSON()).toMatchSnapshot()
);
}
UPDATE:
If you want to specify the validation outside of the component, you could create a prop, say, stateValidation to pass in a the validation function:
jest('loading state', async () => {
fetchMock.get('*', 'Some Content');
jestValidation = () => expect(widget.toJSON()).toMatchSnapshot();
let widget = renderer.create(<Widget stateValidaton={jestValidation}/>);
});
then use the prop in the component:
async componentDidMount() {
var result = await this.getSomeContent()
this.setState(...this.state, {
contents: result
},
// setState callback- fires when state changes are complete.
this.props.stateValidaton
);
}