I have two components who use the same reducer and share the same state.
The first one is a form, the second one is a separate component to
update a specific field of that form using
react-native-autocomplete-select.
In the second component, everything works fine. But when I get back
to the first component (the form), the prop that I'm updating in the
second component is now undefined. Only when I leave the component
and come back to it or reload my app does the component display the
correct value.
I'm new to redux and I thought I had figured it out but apparently, I'm still missing something.
I'll try to share as much code as possible in order to make it easy for anyone to help me out but let me know if you want me to share additional code.
I would really like to understand what's going on.
First Component
class EditElem extends Component {
componentWillMount() {
this.props.xFetch();
}
onButtonPress() {
const { name, description, elem_id } = this.props;
this.props.xSave({ name, description, elem_id });
}
render() {
return (
<ScrollView>
<View>
<Text>Information</Text>
<CardSection>
<Input
label="Name"
placeholder="..."
value={this.props.name}
onChangeText={value => this.props.xUpdate({ prop: 'name', value })}
/>
<Text style={styles.labelStyle}>Description</Text>
<Input
placeholder="Write here..."
value={this.props.description}
onChangeText={value => this.props.xUpdate({ prop: 'description', value })}
multiline = {true}
numberOfLines = {4}
/>
<TouchableWithoutFeedback onPress={ () => Actions.selectElem() }>
<View style={styles.wrapperStyle}>
<View style={styles.containerStyle}>
<Text style={styles.labelStyle}>Elem</Text>
<Text adjustsFontSizeToFit style={styles.inputStyle}>{checkElem(this.props.elem_id ? this.props.elem_id.toString() : "0")}</Text>
</View>
</View>
</TouchableWithoutFeedback>
</CardSection>
<Button title="Save Changes" onPress={this.onButtonPress.bind(this)} />
</View>
</ScrollView>
);
}
}
const mapStateToProps = (state) => {
const { name, description, elem_id } = state.x.x;
return { name, description, elem_id };
};
export default connect(mapStateToProps, { xUpdate, xFetch, xSave })(EditElem);
Second Component
class SelectElem extends Component {
componentWillMount() {
this.props.xFetch();
}
saveElem(suggestion) {
let elem_id = suggestion.id;
let text = suggestion.text
this.props.xUpdate({ prop: 'elem', text })
this.props.xUpdate({ prop: 'elem_id', elem_id })
this.props.xSave({ elem_id });
}
render() {
const suggestions = data
const onSelect = (suggestion) => {
this.saveElem(suggestion);
}
return(
<View style={{flex: 1}}>
<AutoComplete
placeholder={checkElem(this.props.elem_id ? this.props.elem_id.toString() : "0")}
onSelect={onSelect}
suggestions={suggestions}
suggestionObjectTextProperty='text'
value={this.props.elem}
onChangeText={value => this.props.xUpdate({ prop: 'elem', value })}
minimumSimilarityScore={0.4}
/>
</View>
)
}
}
const mapStateToProps = (state) => {
const { elem_id, description, name, elem } = state.x.x;
return { elem_id, description, name, elem };
};
export default connect(mapStateToProps, { xUpdate, xFetch, xSave })(SelectElem);
store
const store = createStore(reducers, {}, compose(applyMiddleware(ReduxThunk)));
reducer
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_X:
return { ...state, x: { ...state.x, name: action.payload.name, description: action.payload.description, elem_id: action.payload.elem_id } };
case UPDATE_X:
return { ...state, x: { ...state.x, [action.payload.prop]: action.payload.value }};
case SAVE_X:
return state;
default:
return state;
}
}
Actions
export const xUpdate = ({ prop, value }) => {
return {
type: UPDATE_X,
payload: { prop, value }
};
};
export const xSave = ({ name, description, elem_id }) => {
return (dispatch) => {
axios({
method: 'post',
url: 'https:xxxxxxxxxxxxxxxxxxxx',
data: {_____________________ }
}
}).then(response => {
dispatch({ type: SAVE_X });
}).catch(error => console.log(error))
};
};
Can you check if UPDATE_X, SAVE_X ... are defined? Do you have the right import statement at the top of the file?
Ok so my problem came from my reducer actualy in my SAVE_X:
I had:
case SAVE_X:
return { state };
Instead of this:
case SAVE_X:
return { ...state, elem: { ...state.elem, elem_id: action.payload.elem_id } };
Related
I'm learning React Redux at the moment and I'm working on a food list in which the user can add and remove items from a Flatlist, up until now I worked on adding the items, which works perfectly, now I'm using the same approach to remove the item from the global state foodList, I use onLongPress to start the function removeFromFoodList in the Diet screen. When I run the code and I proceed to remove the items instead of deleting the single item it deletes all the items in the Flatlist. Thank you for your help.
Diet
class Diet extends Component {
removeItem = () => {
let foodList = this.props.foodList;
this.props.removeFromFoodList(foodList)
}
render() {
return (
<FlatList
data={this.props.foodList}
renderItem={({item}) => (
<View>
<TouchableOpacity
onLongPress={this.removeItem}
>
<Text>{item.foodName}</Text>
<Text>
{item.calories}
</Text>
<MaterialIcons name="arrow-forward-ios" />
</TouchableOpacity>
</View>
)}
keyExtractor={item => item.id}
/>
}
}
function mapStateToProps(store){
return{
foodList: store.userState.foodList
};
}
const mapDispatchToProps = { removeFromFoodList };
export default connect(mapStateToProps, mapDispatchToProps)(Diet);
INDEX
import { ADD_FOOD, REMOVE_FOOD } from "../constants/index";
export const updateFoodList = (foodList) => {
return { type: ADD_FOOD, payload: foodList}
}
export const removeFromFoodList = (foodList) => {
return { type: REMOVE_FOOD, payload: foodList}
}
REDUCERS
import { ADD_FOOD, REMOVE_FOOD } from "../constants";
const initialState = {
foodList: [],
};
export const user = (state = initialState, action) => {
switch (action.type){
case ADD_FOOD:
return{
...state,
foodList: [...action.payload],
}
case REMOVE_FOOD:
return{
...state,
foodList: [...state.foodList.filter((item) => item.id != action.id)],
}
default:
return state
}
};
ARRAY EXAMPLE
Array [
Object {
"calories": "120",
"foodId": 0.8845240802796346,
"foodName": "Rice",
},
]
I'm not sure why it returns as empty, but there are a few problems I see here.
In your reducer:
[...state.foodList.filter((item) => item.id != action.id)]
If the structure of foodlist is as provided:
Object {
"calories": "120",
"foodId": 0.8845240802796346,
"foodName": "Rice",
},
]
Then it has no id key, and even if it does, action.id doesn't exist (only action.type and action.payload exist). Try console logging action and state.foodList under case REMOVE_FOOD: to get more detail.
On the component Diet this.props.foodList contain all data of the flatlist it's ok , you pass it to the flatlist to be rendered , alwais ok , but on each item of Flatlist you have added on each a onLongPress={this.removeItem} the function removeItem execute removeFromFoodList that you put this.props.foodList as a parameters to be removed .. that why all list are removed
to fix this you need to pass a item value to removeItem():
removeItem = (itemToremove) => {
this.props.removeFromFoodList(itemToremove)
}
render() {
return (
<FlatList
data={this.props.foodList}
renderItem={({ item }) => (
<View>
<TouchableOpacity
onLongPress={this.removeItem(item)}
>
<Text>{item.foodName}</Text>
<Text>
{item.calories}
</Text>
<MaterialIcons name="arrow-forward-ios" />
</TouchableOpacity>
</View>
)}
keyExtractor={item => item.id}
/>
)
}
I created a view cart in which I show total price and view cart button, when I add item it makes condition true and display that cart below in every screen, but when I click view cart it's not making it false again, how can I do this? can someone check my code and tell me please. Below is my code
Viewcart.js
<View>
{this.props.show && this.props.items.length > 0 ? (
<View style={styles.total}>
<Text style={styles.totaltext}>Total:</Text>
<Text style={styles.priceTotal}>{this.props.total}</Text>
<View style={styles.onPress}>
<Text
style={styles.pressText}
onPress={() => {
RootNavigation.navigate("Cart");
this.props.show;
}}
>
View Cart
</Text>
</View>
</View>
) : null}
</View>
const mapStateToProps = (state) => {
return {
show: state.clothes.show,
};
};
const mapDispatchToProps = (dispatch) => {
return {
showCart: () => dispatch(showCart()),
};
};
reducer.js
if (action.type === SHOW_CART) {
let addedItem = state.addedItems;
if (addedItem.length === 0) {
return {
...state,
show: state.showCart,
};
} else {
return {
...state,
show: action.showCart,
};
}
}
const initialstate = {
showCart: false
}
action.js
export const showCart = (id) => {
return {
type: SHOW_CART,
showCart: true,
id,
};
};
As per the chat the requirement is to toggle this when exiting the screen so the easiest way to do that is to use the lifecycle methods.
To hide use componentDidMount
componentDidMount(){
this.props.showCartOff();
}
to show use component
componentWillUnmount(){
this.props.showCart();
}
I have trouble trying to retrieve data from AsyncStorage, I can't directly assign a state like that, since it always returns undifined, how can I avoid that?
export default class ListTodo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {},
};
}
componentDidMount() {
//promise
GetDataAsyncStorage('#TODOS').then((data) => {
this.setState({
data: data,
});
});
}
render() {
const {data} = this.state;
console.log(data); // undifined
return (
<>
<Header />
<View>
<FlatList
data={data}
renderItem={({item}) => <TodoItemComponent data={item} />}
keyExtractor={(item) => item.id}
/>
</View>
</>
);
}
}
Here is my function to get data from asynStorage
export const GetDataAsyncStorage = async (key) => {
try {
let data = await AsyncStorage.getItem(key);
return {status: true, data: JSON.parse(data)};
} catch (error) {
return {status: false};
}
};
Add a state variable isLoading and toggle it after the data is got from AsyncStorage
snack: https://snack.expo.io/#ashwith00/async
code:
export default class ListTodo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {},
isLoading: false,
};
}
componentDidMount() {
this.getData();
}
getData = () => {
this.setState({
isLoading: true,
});
//promise
GetDataAsyncStorage('#TODOS').then((data) => {
this.setState({
data: data,
isLoading: false,
});
});
};
render() {
const { data, isLoading } = this.state;
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator />
) : data.data ? (
<FlatList
data={data}
renderItem={({ item }) => <Text>{item}</Text>}
keyExtractor={(item, i) => i.toString()}
/>
) : (
<Text>No Data Available</Text>
)}
</View>
);
}
}
Because AsyncStorage itself is asynchronous read and write, waiting is almost necessary, of course, another way to achieve, for example, to create a memory object, bind the memory object and AsyncStorage, so that you can read AsyncStorage synchronously.
For example, using the following development library can assist you to easily achieve synchronous reading of AsyncStorage react-native-easy-app
import { XStorage } from 'react-native-easy-app';
import { AsyncStorage } from 'react-native';
// or import AsyncStorage from '#react-native-community/async-storage';
export const RNStorage = {
token: undefined,
isShow: undefined,
userInfo: undefined
};
const initCallback = () => {
// From now on, you can write or read the variables in RNStorage synchronously
// equal to [console.log(await AsyncStorage.getItem('isShow'))]
console.log(RNStorage.isShow);
// equal to [ await AsyncStorage.setItem('token',TOKEN1343DN23IDD3PJ2DBF3==') ]
RNStorage.token = 'TOKEN1343DN23IDD3PJ2DBF3==';
// equal to [ await AsyncStorage.setItem('userInfo',JSON.stringify({ name:'rufeng', age:30})) ]
RNStorage.userInfo = {name: 'rufeng', age: 30};
};
XStorage.initStorage(RNStorage, AsyncStorage, initCallback);
new to React/Redux, I am trying to make it so a user can have as many choices with different choice and customAttribute.
For now I have a button they can createUI with to dynamically create a new textfield to input another choice and customAttribute but am completely stumped on how to store such a thing in redux.
I've seen other questions and answers on how to store username and/or password, but have not seen any examples on how to store an object with my full state in it.
My Component
class CreateRIG extends Component<any, any> {
constructor(props) {
super(props);
this.state = {
rigName: '',
desc: '',
choices: [{
choice: '',
customAttribute: ''
}]
}
}
createUI() {
return this.state.choices.map((el, i) => (
<div key={i}>
<FormControl>
<Select
id="choice"
name="choice"
onChange={this.handleChange.bind(this, i)}
value={el.choice || ''}
>
<MenuItem value=''>
<em>None</em>
</MenuItem>
<MenuItem value='ID'>ID</MenuItem>
<MenuItem value='PSA'>PSA</MenuItem>
<MenuItem value='ExternalID'>ExternalID</MenuItem>
</Select>
</FormControl>
<div>
<TextField name="customAttribute" label="Attribute"
onChange={this.handleChange.bind(this, i)} value={el.customAttribute || ''} />
))
}
handleSubmit = (event) => {
event.preventDefault();
console.log(this.state);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div>
<TextField name="rigName"
value={this.state.rigName}
onChange={event => this.setState({ rigName: event.target.value })}/>
</div>
<div className={styles.spacing}>
<TextField name="description"
onChange={event => this.setState({ desc: event.target.value })}/>
</div>
{this.createUI()}
<Button type="submit"
onClick={this.props.onSubmitForm}>NEXT</Button>
</form>
);
}
}
const mapDispatchToProps = dispatch => {
return {
onSubmitForm: (rigName, desc, choice, customAttribute) => dispatch({ type: 'FORM_DATA'})
}
}
const connectedCreateRIGPage = connect(mapStateToProps, mapDispatchToProps)(CreateRIG);
export { connectedCreateRIGPage as CreateRIG };
export default CreateRIG;
Action.tsx
export const createRIGActions = {
searchInput
};
function searchInput(rigName, desc, choice, customAttribute) {
return {
type: createRIGConstants.STORE_FORM,
rigName,
desc,
choice,
customAttribute
}
}
Reducer.tsx
const initialState = {
results: []
};
export function createRIGReducer(state = initialState, action) {
switch (action.type) {
case createRIGConstants.STORE_FORM:
return {
...state,
results: state.results
}
// Need to somehow get the data from the form
default:
return state
}
}
How do you store a complex object from a form, into redux state on submit? Right now my onSubmit is console logging the correct object I want so thats nice
I believe it is just matter what you pass into dispatch. You can add payload there as well.
Try the following:
const mapDispatchToProps = dispatch => {
return {
onSubmitForm: (rigName, desc, choice, customAttribute) => dispatch({ type: 'FORM_DATA', payload: {rigName, desc, choice, customAttribute}})
}
}
Then in your reducer you can access that payload like the following:
export default (state=initialState, action) => {
switch(action.type) {
const { payload } = action;
case 'FORM_DATA':
return {
...state,
// here you can use data from payload to update the state
};
Read further here from Redux documentation: Managing Normalized Data
I hope this helps!
My problem is that my flat list is not being updated when I add an element to "entriesPerDay" in my Redux store.
Both home screen and the Flat List have state mapped to props. I have tried:
- passing the data to EntriesList through props from Home Screen
- using the state from reducer as data provider of the EntriesList
Nothing seem to be working and the shouldComponentUpdate or any other relevant function is never called.
REDUCER:
case NEW_ENTRY_LOCAL:
let newState = {...state};
newState.entriesPerDay.push(action.entry);
return{
...newState
}
HOME SCREEN:
(...)
render() {
return (
<View style={{ flex: 1, alignItems: 'flex-start', justifyContent: 'flex-start' }}>
<NavBar onItemPressed={this.onItemPressedHandler}/>
<DayPicker onDatePicked={this.onDatePickedHandler} />
<TotalTime totalTime={this.props.total} />
<EntriesList entries={this.props.entriesPerDay}/>
<Timer onEntryEnd={this.onEntryEndHandler} onTimeChanged={this.onTimeChangedHandler} onEntryCreate={this.onEntryCreateHandler}/>
<Button title="clickkk" onPress={this.onItemPressedHandler}>CLICK ME</Button>
</View>
);
}
FLAT LIST:
class entriesList extends React.Component {
componentDidMount()
{
reactMixin(entriesList, TimerMixin);
this.timer = setInterval(() => {
console.log(this.props.entriesPerDay);
}, 3000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
shouldComponentUpdate(nextProps, nextState)
{
console.log("new props" + nextProps);
return true;
}
render()
{
return (
<FlatList style={styles.full}
data={this.props.entries}
extraData={this.props.entriesPerDay}
renderItem={
(info)=>(
<ListItem key={info.item.key}
startDateTime={info.item.startDateTime}
endDateTime = {info.item.endDateTime}
description = {info.item.description}
prevEntryEnd = {info.item.prevEntryEnd}
nextEntryStart = {info.item.nextEntryStart}
total = {info.item.totalTime}
/>
)
}
/>
);
}
}
const mapStateToProps = state => {
return {
serverCopy : state.entries.serverCopy,
entriesPerDay : state.entries.entriesPerDay,
pickedDate : state.entries.pickedDate,
total: state.entries.total,
local: state.entries.local
};
};
const mapDispatchToProps = dispatch => {
return {
onPickDate: (day) => dispatch(pickDay(day)),
onDataSet: (data) => dispatch(setData(data)),
onNewEntryLocal: (entry) => dispatch(newEntryLocal(entry)),
onEndEntryLocal: (entry) => dispatch(endEntryLocal(entry)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(entriesList)
;
Try :
case NEW_ENTRY_LOCAL:
let newEntriesPerDay = state.entriesPerDay.concat(action.entry);
return {
...state,
entriesPerDay: newEntriesPerDay
}
It is because entriesPerDay was just beeing copied by reference in the newState in your previous example. redux and react compare the reference and see that it's the same so no re-render will happen. That's why you should copy it manually.
Note : Use concat instead of push to return a new array (new reference)