I have a Picker component like this:
const reasons = [
{ name: 'A' },
{ name: 'B' },
{ name: 'Other' }
];
<Picker
selectedValue={state.reasonSelected}
onValueChange={value => {
changeTransactionReason(value);
}}
>
{reasons.map((reason, key) => (
<Picker.Item
key={key}
label={reason.name}
value={reason.name}
/>
))}
</Picker>
Then, I need to render an Input component only if the item "Other" is picked.
I could setState that value and then ask if:
{state.itemSelected === 'Other' && (
<Input onChangeText={text => { reduxActionToSaveValue(text) }}/>
)}
But I don't want to mix local state and redux. The values will be stored on redux, but
How can I compare the value 'Other' without losing it actual value? So I want to know if there's a way with Picker to get the selected value without setting local states
Why not compare the value directly with redux? if you are using react-redux you can map the value where selected item is stored using mapStateToProps (here is the documentation) and if you are directly using redux you can still do it. then once the action of choosing other has dispatched and the value is set your input will show.
if you don't want to use redux, you need to get the Other value from props and initialize the value on the picker.
Regards
Only with redux I would do this:
I would create a reducer reasons
const intialState = {
reasonSelected: '1',
reasons: [{name: '1'}, {name: '2'}, {name: '3'}],
};
const reasons = (state = intialState, {type, payload}) => {
switch (type) {
case 'CHANGE_SELECTED':
return payload;
default:
return state;
}
};
export default reasons;
In this reducer I would initialize values and the selecteddefault and I would create an action CHANGE_SLECTEd to change the value selected.
and in the component I would call the reducer, I called Dashboard.
import React from 'react';
import {View, Text, Picker} from 'react-native';
import {useSelector, useDispatch} from 'react-redux';
const Dashboard = () => {
const reasonObj = useSelector((state) => state.reasons);
const dispatch = useDispatch();
const onValuePickChange = (value) => {
dispatch({
type: 'CHANGE_SELECTED',
payload: {
reasonSelected: value,
reasons: reasonObj.reasons,
},
});
};
return (
<View>
<Text>Dashboard</Text>
<Picker
selectedValue={reasonObj.reasonSelected}
onValueChange={(value) => onValuePickChange(value)}>
{reasonObj.reasons.length > 0 &&
reasonObj.reasons.map((reason, key) => (
<Picker.Item key={key} label={reason.name} value={reason.name} />
))}
</Picker>
</View>
);
};
export default Dashboard;
inthis form you are not using local state and everything is immediately update to redux.
I hope this info will help you...
Regards
Related
I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.
I'm creating my own searchbar in react-native, so I want to allow user clear the text input. To achieve this I'm using useState to track when the text input value is empty or not. I'm executing onStateChange('') to clean the text input value, the problem is if I access to value this show the value before onStateChange('') not empty string ''.
e.g. If I type react on input and after that execute onStateChange('') I expect the input value property to be empty, but it is not, value contains react yet, If I press other key the value is '' and not the current one. i.e. it is not saving the last value entered but the previus one.
SearchBar Usage
import React, { useState } from 'react';
import {
TextInput,
TouchableWithoutFeedback,
View,
} from 'react-native';
function SearchBar(props) {
const {
onChangeText,
value = '',
...textInputProps
} = props;
// Access to native input
const textInputRef = React.createRef();
// Track if text input is empty according `value` prop
const [isEmpty, setIsEmpty] = useState(value ? value === '' : true);
const clear = () => {
// Clear the input value with native text input ref
textInputRef.current?.clear();
// Update the text input `value` property to `''` i.e. set to empty,
if (onChangeText) onChangeText('');
// this is where the problem lies, because If I check the input text `value` property, this show the previus one, not the current one.
console.log(value);
};
const handleOnChangeText = (text) => {
if (onChangeText) onChangeText(text);
// track if text input `value` property is empty
setIsEmpty(text === '');
};
// Render close icon and listen `onPress` event to clear the text input
const renderCloseIcon = (iconProps) => (
<TouchableWithoutFeedback onPress={clear}>
<View>
<Icon {...iconProps} name="close" pack="material" />
</View>
</TouchableWithoutFeedback>
);
return (
<TouchableWithoutFeedback onPress={focus}>
<View>
<TextInput
{...textInputProps}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChangeText={handleOnChangeText}
ref={textInputRef}
/>
// helper function to render a component with props pased to it
<FalsyFC
style={{ paddingHorizontal: 8 }}
// If text input `value` is `isEmpty` show the `x` icon
component={!isEmpty ? renderCloseIcon : undefined}
/>
</View>
</TouchableWithoutFeedback>
);
}
SearchBar Usage
function SomeComponent() {
const [search, setSearch] = useState('');
const updateSearch = (query: string) => {
setSearch(query);
};
return (
<SearchBar
onChangeText={updateSearch}
value={search}
/>
)
}
I think you are missing a value prop in the TextInput (see React Native doc for TextInput).
In your case you would set your value variable from the props object you pass to the SearchBar functional component:
<TextInput
{...textInputProps}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChangeText={handleOnChangeText}
ref={textInputRef}
// add the line below
value={value}
/>
Every state changes are asynchronous even Hooks. With useState() you can't define a callback function even onStateChange() is not called. In your case, you should use the function useEffect().
function SomeComponent() {
const [search, setSearch] = useState('');
useEffect(() => {
//Write your callback code here
//copy-paste what you wrote on function onStateChange()
}, [search]);
const updateSearch = (query: string) => {
setSearch(query);
};
return (
<SearchBar
onChangeText={updateSearch}
value={search}
/>
)
}
Here is a link that should help you :
https://dmitripavlutin.com/react-useeffect-explanation/#:~:text=callback%20is%20the%20callback%20function,dependencies%20have%20changed%20between%20renderings.
I am new to react native and redux.
In this file friends.js, I have already made it so that the app adds a friend when someone taps the "Add Friend" button. I am now also trying to make a form that adds a new name to a list of names. Here is the form:
import React from 'react';
import { StyleSheet, Text, View, Button, TextInput, KeyboardAvoidingView } from 'react-native';
import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import {addFriend} from './FriendActions';
import {addFriendFromForm} from './FriendActions';
class Friends extends React.Component {
constructor(){
super();
this.formFieldValue = "";
}
render() {
return (
<KeyboardAvoidingView behavior="padding" enabled>
<Text>Add friends here!</Text>
{this.props.friends.possible.map((friend,index) => (
<Button
title={`Add ${friend}`}
key={friend}
onPress = { () =>
this.props.addFriend(index)
}
/ >
)
)}
<Button
title="Back to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<TextInput
style={{height: 40, width: 200}}
placeholder="Type in a friend's name"
onChangeText={(text) => {
this.formFieldValue = text;
console.log(this.formFieldValue);
}
}
/>
<Button
title="Add Friend From Form"
onPress={() => this.props.addFriendFromForm(this.formFieldValue)}
/ >
</KeyboardAvoidingView>
);
}
}
const mapStateToProps = (state) => {
const friends = state;
console.log("friends", JSON.stringify(friends));
return friends
};
const mapDispatchToProps = dispatch => (
bindActionCreators({
addFriend,
addFriendFromForm
}, dispatch)
);
export default connect(mapStateToProps, mapDispatchToProps)(Friends);
Here is the action that the form triggers:
export const addFriend = friendIndex => ({
type: 'ADD_FRIEND',
payload: friendIndex
})
export const addFriendFromForm = friendNameFromForm => ({
type: 'ADD_FRIEND',
payload: friendNameFromForm
})
And here is the reducer that calls the actions . (WHERE I THINK THE PROBLEM IS):
import {combineReducers} from 'redux';
const INITIAL_STATE = {
current: [],
possible: [
'Allie',
'Gator',
'Lizzie',
'Reptar'
]
};
const friendReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'ADD_FRIEND':
const {
current,
possible
} = state;
const addedFriend = possible.splice(action.payload, 1);
current.push(addedFriend);
const newState = {current, possible};
return newState;
case 'ADD_FRIEND_FROM_FORM':
const {
currents,
possibles
} = state;
currents.push(action.payload);
const newState2 = {current: currents, possible: possibles};
return newState2;
default:
return state;
}
};
export default combineReducers({
friends: friendReducer
});
As I mentioned, pressing the button to automatically add a name from the initial state works. However, when adding a name through the form, it seems that the form will just add allie to possible friends instead of the name typed into the form because the console.log in friends.js shows this: friends {"friends":{"current":[["Allie"]],"possible":["Gator","Lizzie","Reptar"]}}.
How do I get the form to work?
I used this.formFieldValue in friends.js as it seems like overkill to change the state whenever the form field text changes when we really only need it when the submit button is pressed. Is this the proper way to go about sending the form data to the action?
While it may seem liike I'm asking 2 questions, I'm pretty sure it's one answer that will tie them both togethor. Thanks!
The issue might be that you're directly mutating the state with push, try using array spread instead:
const friendReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'ADD_FRIEND':
const {
current,
possible
} = state;
const addedFriend = possible.splice(action.payload, 1);
current.push(addedFriend);
const newState = {current, possible};
return newState;
case 'ADD_FRIEND_FROM_FORM':
return {...state, current: [...state.current, action.payload]}
default:
return state;
}
};
Also it's a good idea to be consistent with the property names, it seems you're using current and currents interchangeably.
I'm using react-navigation and Unstated in my react native project.
I have a situation where I would like use:
this.props.navigation.navigate("App")
after successfully signing in.
Problem is I don't want it done directly from a function assigned to a submit button. I want to navigate based upon a global Unstated store.
However, it means that I would need to use a conditional INSIDE of the Subscribe wrapper. That is what leads to the dreaded Warning: Cannot update during an existing state transition (such as within 'render').
render() {
const { username, password } = this.state;
return (
<Subscribe to={[MainStore]}>
{({ auth: { state, testLogin } }) => {
if (state.isAuthenticated) {
this.props.navigation.navigate("App");
return null;
}
console.log("rendering AuthScreen");
return (
<View style={styles.container}>
<TextInput
label="Username"
onChangeText={this.setUsername}
value={username}
style={styles.input}
/>
<TextInput
label="Password"
onChangeText={this.setPassword}
value={password}
style={styles.input}
/>
{state.error && (
<Text style={styles.error}>{state.error.message}</Text>
)}
<Button
onPress={() => testLogin({ username, password })}
color="#000"
style={styles.button}
>
Sign in!
</Button>
</View>
);
}}
</Subscribe>
);
It works. But what's the correct way to do it?
I don't have access to MainStore outside of Subscribe and therefore outside of render.
I'm not sure about the react-navigation patterns but you could use a wrapper around this component which subscribes to 'MainStore' and pass it down to this component as a prop. That way you'll have access to 'MainStore' outside the render method.
I have since found a better solution.
I created an HOC that I call now on any Component, functional or not, that requires access to the store. That give me access to the store's state and functions all in props. This means, I am free to use the component as it was intended, hooks and all.
Here's what it looks like:
WithUnstated.js
import React, { PureComponent } from "react";
import { Subscribe } from "unstated";
import MainStore from "../store/Main";
const withUnstated = (
WrappedComponent,
Stores = [MainStore],
navigationOptions
) =>
class extends PureComponent {
static navigationOptions = navigationOptions;
render() {
return (
<Subscribe to={Stores}>
{(...stores) => {
const allStores = stores.reduce(
// { ...v } to force the WrappedComponent to rerender
(acc, v) => ({ ...acc, [v.displayName]: { ...v } }),
{}
);
return <WrappedComponent {...allStores} {...this.props} />;
}}
</Subscribe>
);
}
};
export default withUnstated;
Used like so in this Header example:
import React from "react";
import { Text, View } from "react-native";
import styles from "./styles";
import { states } from "../../services/data";
import withUnstated from "../../components/WithUnstated";
import MainStore from "../../store/Main";
const Header = ({
MainStore: {
state: { vehicle }
}
}) => (
<View style={styles.plateInfo}>
<Text style={styles.plateTop}>{vehicle.plate}</Text>
<Text style={styles.plateBottom}>{states[vehicle.state]}</Text>
</View>
);
export default withUnstated(Header, [MainStore]);
So now you don't need to create a million wrapper components for all the times you need your store available outside of your render function.
As, as an added goodie, the HOC accepts an array of stores making it completely plug and play. AND - it works with your navigationOptions!
Just remember to add displayName to your stores (ES-Lint prompts you to anyway).
This is what a simple store looks like:
import { Container } from "unstated";
class NotificationStore extends Container {
state = {
notifications: [],
showNotifications: false
};
displayName = "NotificationStore";
setState = payload => {
console.log("notification store payload: ", payload);
super.setState(payload);
};
setStateProps = payload => this.setState(payload);
}
export default NotificationStore;
I am having big troubles getting the "updated" value of a record in an edit form. I always get the initial record values, even though I have an input linked to the right record source, which should update it.
Is there an alternative way to get the values of the SimpleForm ?
I have a simple edit form :
<Edit {...props}>
<SimpleForm>
<MyEditForm {...props} />
</SimpleForm>
</Edit>
MyEditForm is as follow:
class MyEditForm extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps.record.surface, this.props.record.surface); // <-- here is my problem, both values always get the initial value I had when I fetched the resource from API
}
render() {
return (
<div>
<TextInput source="surface" />
<!-- other fields -->
</div>
);
}
}
I usually do it this way to get my updated component's data from other components, but in the very case of a react-admin form, I can't get it to work.
Thanks,
Nicolas
It really depends on what you want to do with those values. If you want to hide/show/modify inputs based on the value of another input, the FormDataConsumer is the preferred method:
For example:
import { FormDataConsumer } from 'react-admin';
const OrderEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<SelectInput source="country" choices={countries} />
<FormDataConsumer>
{({ formData, ...rest }) =>
<SelectInput
source="city"
choices={getCitiesFor(formData.country)}
{...rest}
/>
}
</FormDataConsumer>
</SimpleForm>
</Edit>
);
You can find more examples in the Input documentation. Take a look at the Linking Two Inputs and Hiding Inputs Based On Other Inputs.
However, if you want to use the form values in methods of your MyEditForm component, you should use the reduxForm selectors. This is safer as it will work even if we change the key where the reduxForm state is in our store.
import { connect } from 'react-redux';
import { getFormValues } from 'redux-form';
const mapStateToProps = state => ({
recordLiveValues: getFormValues('record-form')(state)
});
export default connect(mapStateToProps)(MyForm);
I found a working solution :
import { connect } from 'react-redux';
const mapStateToProps = state => ({
recordLiveValues: state.form['record-form'].values
});
export default connect(mapStateToProps)(MyForm);
When mapping the form state to my component's properties, I'm able to find my values using :
recordLiveValues.surface
If you don't want to use redux or you use other global state like me (recoil, etc.)
You can create custom-child component inside FormDataConsumer here example from me
// create FormReceiver component
const FormReceiver = ({ formData, getForm }) => {
useEffect(() => {
getForm(formData)
}, [formData])
return null
}
// inside any admin form
const AdminForm = () => {
const formState = useRef({}) // useRef for good performance not rerender
const getForm = (form) => {
formState.current = form
}
// you can access form by using `formState.current`
return (
<SimpleForm>
<FormDataConsumer>
{({ formData, ...rest }) => (
<FormReceiver formData={formData} getForm={getForm} />
)}
</FormDataConsumer>
</SimpleForm>
)
}