How would I pass a getParam param to another screen using react-navigation in a stateless component to fire up a graphql mutation? - javascript

Following the docs: react-navigation params, I'd basically do something like this (parent):
this.props.navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
And then passing to the child
class DetailsScreen extends React.Component {
render() {
const { navigation } = this.props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
return ( (..omited for brevity)
EDIT:
My situation is that in one screen/container we fire up a graphql mutation that takes a phoneNumber and retrieves a code to user.
Onto the next screen I need to take this phoneNumber that the user has just inserted to fire another mutation that takes phoneNumber and code to make the authentication. I don't want to use states to do that, since there's api available on react-navigation.
Parent:
signInUser(phoneNumber: string) {
const { navigation, sendCode } = this.props
sendCode({ variables: { phoneNumber } }) // graphql mutation
navigation.navigate('Authenticate', { phoneNumber }) // passing
// phoneNumber as param
}
Child:
export const AuthenticationCodeModal = (props: NavigationScreenProps) => {
const {navigation} = props
const phoneNumber = navigation.getParam(phoneNumber)
// console.log(phoneNumber)
// I need to get phoneNumber here and pass it as props to
// <AuthenticationFormContainer> which will fire up auth mutation
return(
<View style={styles.container} >
<AuthenticationFormContainer/>
</View>
)
}

It better and interesting if you use redux for task like this. Redux has a global state which can be assessed by any component

Turns out it was a simple syntax error of the value that getParam receives, doing this fixed my problem:
navigation.navigate('Authenticate', { phoneNumber: {phoneNumber: phoneNumber} })

Related

How to indicate to TS that value in store has value even though it's optional?

I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};

Any change to redux store my causes component to re-render

I'm doing some testing on my UI and I've noticed that if any state changes in my redux store my component (shown below) re-renders and restarts with embedded video at 0. If I type in a redux-connected text field, it remounts, if a status notification hits the store, it remounts, etc.
I have no idea how to fix this and I could really use some help figuring out how to go after the bug.
tldr; How can I stop my VideoPlayer from re-rendering each time something changes in my redux store?
redux-toolkit
react
component
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm);
// renders output here in the same component
return (
...
{sourceContentFound === false ? (
<VideoPlayerDisabled />
) : (
<VideoPlayerController
title={title}
description={description}
sourceAddress={iframeURL}
author={authorName}
timeStamp={{ startTime: 0 }}
/>
)}
)
...
}
formSlice
export const dynamicFormSlice = createSlice({
name: 'dynamicForm',
initialState,
reducers: {
updateField: (state, action) => {
state = action.payload;
return state;
}
},
});
export const selectDynamicForm = createSelector(
(state: RootState): dynamicFormState => state.dynamicForm,
dynamicForm => dynamicForm
);
statusHandlerSlice
I don't think this component does anything crazy, per-say, but I have a notification appear when the video conditions are met. When it goes back down clearStatus the video player restarts.
export const statusHandlerSlice = createSlice({
name: 'statusHandler',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<IStatusObject>) => {
const { type, display, message } = action.payload;
state.status = {
...action.payload,
message: message.charAt(0).toUpperCase() + message.slice(1),
};
if (display === 'internal-only' || display === 'support-both') {
statusLogger(type, message);
}
},
clearStatus: state => {
state.status = {
type: 'success',
data: {},
message: '',
display: 'internal-only',
key: '',
};
},
},
});
export const { setStatus, clearStatus } = statusHandlerSlice.actions;
export const selectStatus = (state: RootState): IStatusObject =>
state.statusHandler.status;
Your MyComponent is re-render every time redux store state change is because you have a selector in it
You could stop this to happen by, add an equalityFn to useSelector.
You can write your own equalityFn or use some existing function from a library that supports deep comparison.
Ex: Use lodash isEqual
import { isEqual } from 'lodash';
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm, isEqual);
By default, useSelector use a shallow compare which can't detect deep changes inside your object, change to a deep comparison function like isEqual will help you to do that, but It's not recommended for all selector since there will be a performance impact.
Live Example:
I suggest either creating a custom equalFn to compare the data you're using in the current component or do not select the whole slice, maybe some properties change is unnecessary for your component. like:
const { data } = useSelector(store => store.sliceA, shallowEq);
// console.log(data) => { a: "foo", b: "bar" }
// data.b is useless but once it is changed, the component will re-render as well
return <Typography>{data.a}</Typography>
You should install React Devtools, turn on profiler, remember to check Record why each component rendered while profiling in setting to see what is causing re-rendering. sometimes custom hooks in libraries trigger re-rendering.
whyDidYouRender
is a good choice too

Use an async function from one component to another

I'm working on coding a Dapp with the react boxe from truffle. For that application I need to use a function (just two getters) from my smart contract in another component than App.js. This function is the getUser, which needs to be in form from Login.js.
App.js:
class App extends Component {
state = {
web3: null,
contract: undefined,
account: null,
user: {
id: null,
name: '',
password: ''
}
};
componentDidMount = async () => {
try {
const web3 = await getWeb3();
const accounts = await web3.eth.getAccounts();
const networkId = await web3.eth.net.getId();
const deployedNetwork = Login.networks[networkId];
const instance = new web3.eth.Contract(
Login.abi,
deployedNetwork && deployedNetwork.address,
);
this.setState({ web3, accounts, contract: instance });
} catch (error) {
alert(
`Failed to load web3, accounts, or contract.
Check console for details.`,
);
console.error(error);
}
};
getUser = async (event) => {
const userID = this.state.id;
const userName = await this.state.contract.methods
.getUsername(userID).call();
const userPassword = await this.state.contract.methods
.getPassword(userID).call();
console.log(userName + ' - ' +userPassword);
};
render() {
if (!this.state.web3) {
return <div>Loading Web3, accounts, and contract...</div>;
}
return (
<div className="App">
</div>
);
}
}
export default App;
Login.js:
class App extends Component{
handleInputChange = (event) => {
let input = event.target;
let name = event.target.name;
let value = input.value;
this.setState({
[name]: value
})
}
render() {
return (
<div className='Login'>
<form>
<label>
<span>Barcode:</span>
<input name="id" type="number" required
onChange={this.handleInputChange} />
</label>
<button type="submit" value="submit">Get user data</button>
</form>
</div>
);
}
}
export default Login;
I first tried to export the getUser function:
App.js:
export const getUser = async (event) => {
const userID = this.state.id;
const userName = await this.state.contract.methods
.getUsername(userID).call();
const userPassword = await this.state.contract.methods
.getPassword(userID).call();
console.log(userName + ' - ' +userPassword);
};
Login.js:
import {getUser} from ...
But I got the following error:
Unhandled Rejection (TypeError): Cannot read property 'state' of undefined
I then tried to add inside the Login, the same componentDidMount
and state found inside App.js (with the imports), however I got
the following error:
TypeError: Cannot read property 'methods' of null
I no longer have any idea of how to do it. So I would like to ask some help please.
I thank in advance anyone who will take the time to help me.
You just need to pass the value between components. Let's say onClick you will move from login to app component and you will pass id & name.
onClick=(id,name)=>{
history.push({
pathname: '/secondpage', //app component url
state: { id: id, name: name }
})
}
history could be imported from react-router
Then in app component you can access this value in, this.props.location.state
If you want to send value through component, in login component declare app component like this,
<App
id={id}
name={name}
/>
Then in app component you can get the value in this.props
Global State Management
You are looking for a way to manage state at a global level. There are lots of solutions for this problem that exists and are widely used.
Solution 1: Props or "prop drilling"
The idea is to pass the state to every component that needs it. You should only consider doing this if you are passing down props that are not global level or is needed by only a few component trees.
An example of passing down props is in the official docs (https://reactjs.org/docs/context.html)
Solution 2: React Context API
There is already a built in feature inside React that is widely used called Context API that solves this. Example of usage is also in the docs. https://reactjs.org/docs/context.html
Context provides a way to pass data through the component tree without having to
pass props down manually at every level.
TLDR; If you need to your state to be at a global level where lots of component trees need access to it, this is probably the solution.

How to call a setState with data from props? React Native

I have a component Home that is called after user get in. There are some data in that screen, and in the header I have a icon button that send the user for a screen where the user can see their profile data and also delete account. So when the icon button is clicked I'm sending data using props.navigation, sending the user to another screen/component.
profile = () => {
const { navigation } = this.props;
const user = navigation.getParam('user', 'erro');
this.props.navigation.navigate('profileScreen', { user: user });
}
Inside the new component, I tried to setState inside the method componentDidMount using that data but it didn't work. I checked using console.log the data is there. How could I setState in this case?
export default class profileScreen extends Component {
static navigationOptions = {
title: "Profile"
};
constructor(props) {
super(props);
this.state = {
user: {}
}
}
componentDidMount() {
const {navigation} = this.props;
const user = navigation.getParam('user', 'Erro2');
this.setState({user: user.user});
console.log(this.state); // Object {"user": Object {},}
console.log("const user");
console.log(user.user); //my actual data is printed
}
render() {
return (
<Text>{this.state.use.name}</Text>
<Text>{this.state.use.age}</Text>
<Text>{this.state.use.email}</Text>
...
...
)
}
}
Result from console.log(this.state)
Object {
"user": Object {},
}
Result from console.log(user)
Object {
"createdAt": "2019-04-27T21:21:36.000Z",
"email": "sd#sd",
"type": "Admin",
"updatedAt": "2019-04-27T21:21:36.000Z",
...
...
}
It seems like you're trying to send an object (user) as a route parameter with react-navigation library. It's not possible.
The proper way of doing such scenarios is to send the user's id userId as route parameter and load user details from your API (or state).
profile = () => {
const user = {id: 10 /*, ... rest of properties */}; // or take it from your state / api
this.props.navigation.navigate('profileScreen', { userId: user.id });
}
componentDidMount() {
const {navigation} = this.props;
const userId = navigation.getParam('userId', 'Erro2');
// const user = read user details from state / api by providing her id
this.setState({user: user});
}
ps: if you are using state management like redux/flux/..., consider setting currentUser in your global state and read that instead of passing userId as a route param.
To make sure component updates when the user got new value in the state render method should be like this:
render() {
const {user} = this.state
return (
<View>
{user && <Text>{user.name}</Text>}
{user && <Text>{user.age}</Text>}
{user && <Text>{user.email}</Text>}
...
...
</View>
)
}
Note 0: const {user} = this.state would save you from repeating this.state
Note 1: it would much more elegant to wrap all those <Text> component inside another <View> to prevent repeating the condition phrase {user && ...}

Best approach for using same component for editing and adding data. Mixing component state with redux store

I'm building web app in React with Redux. It is simple device manager. I'm using the same component for adding and updating device in database. I'm not sure, if my approach is correct. Here you can find parts of my solution:
UPDATE MODE:
In componentDidMount I'm checking, if deviceId was passed in url (edit mode). If so, I'm calling redux action to get retrieve data from database. I'm using connect function, so when response arrives, It will be mapped to component props.
Here is my mapStateToProps (probably I should map only specific property but it does not matter in this case)
const mapStateToProps = state => ({
...state
})
and componentDidMount:
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
Next, componentWillReceiveProps will be fired and I will be able to call setState in order to populate inputs in my form.
componentWillReceiveProps(nextProps) {
if (nextProps.devices.item) {
this.setState({
id: nextProps.devices.item.id,
name: nextProps.devices.item.name,
description: nextProps.devices.item.description
});
}
}
ADD MODE:
Add mode is even simpler - I'm just calling setState on each input change.
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
That's how my inputs looks:
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={this.state.description}
/>
I don't like this approach because I have to call setState() after receiving data from backend. I'm also using componentWillReceiveProps which is bad practice.
Are there any better approaches? I can use for example only redux store instead of component state (but I don't need inputs data in redux store). Maybe I can use React ref field and get rid of component state?
Additional question - should I really call setState on each input onChange?
To avoid using componentWillReceiveProps, and because you are using redux, you can do:
class YourComponent extends React.Component {
state = {
// ...
description: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.description === undefined && nextProps.description) {
return { description: nextProps.description };
}
return null;
}
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
// ...
render() {
let { description } = this.state;
description = description || ''; // use this value in your `TextField`.
// ...
return (
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={description}
/>
);
}
}
const mapStateToProps = (state) => {
let props = { ...state };
const { devices } = state;
if (devices && devices.item) {
props = {
...props,
id: devices.item.id,
name: devices.item.name,
description: devices.item.description,
};
}
return props;
};
export default connect(
mapStateToProps,
)(YourComponent);
You can then access id, name, and description thought this.props instead of this.state. It works because mapStateToProps will be evaluated every time you update the redux store. Also, you will be able to access description through this.state and leave your TextField as is. You can read more about getDerivedStateFromProps here.
As for your second question, calling setState every time the input changes is totally fine; that's what's called a controlled component, and the react team (nor me) encourage its use. See here.

Categories

Resources