React When does rendering happen - reactjs

My project use dvajs(Based on redux and redux-saga), The code below is to send a request after clicking the button, change the status through connect, and then call the ant design component message.error an message.success(Similar to alert) to remind
import type { Dispatch } from 'umi';
import ProForm, { ProFormText } from '#ant-design/pro-form';
import { message } from 'antd';
const tip = (type: string, content: string) => {
if (type === 'error') message.error(content, 5);
else message.success(content, 5);
};
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
return (
<div>
<ProForm
onFinish={(values) => {
handleSubmit(values as RegisterParamsType);
return Promise.resolve();
}}
>
<ProFormText/>
...
{
status === '1' && !submitting && (
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
)
)
}
<<ProForm>/>
</div>
)
}
const p = ({ register, loading }: { register: RegisterResponseInfo, loading: Loading; }) => {
console.log(loading);
return {
registerResponseInfo: register,
submitting: loading.effects['register/register'],
};
};
export default connect(p)(RegisterFC);
When I click the button, the console prompts:
Warning: Render methods should be a pure function of props and state;
triggering nested component updates from render is not allowed. If
necessary, trigger nested updates in componentDidUpdate.
Doesn't the component re-render when the state changes? Does the tip function change the state?

Solution: Call tip Outside of return
tip is just a function that you are calling. You should call it outside of the return JSX section of your code. I think it makes the most sense to call it inside of a useEffect hook with dependencies on status and submitting. The effect runs each time that status or submitting changes. If status is 1 and submitting is falsy, then we call tip.
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
React.useEffect(() => {
if (status === '1' && !submitting) {
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
);
}
}, [status, submitting]);
return (
<div>...</div>
)
}
Explanation
Render methods should be a pure function of props and state
The render section of a component (render() in class component or return in a function component) is where you create the JSX (React HTML) markup for your component based on the current values of props and state. It should not have any side effects. It creates and returns JSX and that's it.
Calling tip is a side effect since it modifies the global antd messsage object. That means it shouldn't be in the render section of the code. Side effects are generally handled inside of useEffect hooks.
You are trying to conditionally render tip like you would conditionally render a component. The problem is that tip is not a component. A function component is a function which returns a JSX Element. tip is a void function that returns nothing, so you cannot render it.

Related

Redux toolkit mutation access to state across multiple components

I'm start using RTQ.
To access query data is possible via generated hook. But for mutations, it doesn't work.
I have this simple example.
This component is responsible for loading "users" from API.
const Page = () => {
const {
isSuccess: isSuccessUsers
isLoading: isLoadingUsers
} = useGetUsersQuery({})
if (isLoadingUsers) return <LoadingSpinner />
if (isSuccessUsers) return <UsersTable />
...
}
This component display users, which are obtained from cache - was filled by the API call in previous component "Page".
Also in that component I want display loading spinner for deleting state.
const UsersTable = () => {
const {
data: dataUsers,
isFetching: isFetchingUsers,
} = useGetUsersQuery({})
const [, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
useEffect(() => {
if (isSuccess) {
console.log(data.data.message)
}
}, [isSuccess])
useEffect(() => {
if (isError) {
console.log(error.data.message)
}
}, [isError])
if (usersIsFetching || isLoadingDeleteUser) return <LoadingSpinner />
dataUsers.map(user=>(<UsersTableRow user={user}/>))
}
In this component I only want call delete function. But here, when use same pattern just like for query, it doesnt work.
const UsersTableRow = ({user}) => {
const [deleteUser] = useDeleteUserMutation()
const handleDeleUser = () => {
deleteUser(user.id)
}
...
}
I could solve this by pass deleteUser function as prop from UsersTable component to UsersTableRow component.
const [ deleteUser, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
dataUsers.map(user=>(<UsersTableRow deleteUser={deleteUser} user={user}/>))
I would like to avoid this, because if there are more child components, I will have to pass to every child.
Is there some better solution?
You can use a fixed cache key to "tell" redux-toolkit that you want to use the same result for both components:
export const ComponentOne = () => {
// Triggering `deleteUser` will affect the result in both this component,
// but as well as the result in `ComponentTwo`, and vice-versa
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}
export const ComponentTwo = () => {
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}

onChange return undefined in ReactJS

I have this component:
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
import JSONEditor from "jsoneditor";
import "jsoneditor/dist/jsoneditor.css";
const JSONReact = ({ json, mode, onChange }) => {
const ref1 = useRef(null);
const ref2 = useRef(null);
useEffect(() => {
const props = {
onChangeText: (value) => {
console.log(value, "vv");
onChange(value);
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
if (json) {
ref1.current.set(json);
}
return () => {
ref1.current.destroy();
};
}, []);
useEffect(() => {
if (json) {
ref1?.current.update(json);
}
}, [json]);
return <div ref={ref2} />;
};
export default function App() {
const [state, setState] = useState('{"cars": "22w-08w-23"}');
const onChange = (j) => {
console.log(j);
setState(j);
};
return (
<div className="App">
<JSONReact mode="code" onChange={onChange} json={JSON.parse(state)} />
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
When i type something inside editor i get undefined in console.log(j);, but i don't understand why. Who can help to fix it?
https://codesandbox.io/s/admiring-khorana-pdem1l?file=/src/App.js:909-929
Well it is expected as in the docs it clearly says "This callback does not pass the changed contents", over here https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#:~:text=This%20callback%20does%20not%20pass%20the%20changed%20contents
However, There are two ways to do it, that I found from here
https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options
First Way: use getText() from JSONEditor object to get the current value, since it will return you string so just parse it in json and pass inside your callback (onChange) prop.
JavaScript
const props = {
onChange: () => {
onChange(JSON.parse(ref1.current.getText()));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
Second way: just use onChangeText and it will give the string in the callback,
const props = {
onChangeText: (value) => {
onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 1: added JSON validation check before calling onChange Props
const isValidJSON = (jsonString) => {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
};
useEffect(() => {
const props = {
onChangeText: (value) => {
isValidJSON(value) && onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 2: adding error validation using validate(), here in this case we can remove isValidJSON(value) check from below code since we are already checking for JSON errors using validate().
onChangeText: (value) => {
const errors = ref1.current.validate();
errors.then((err) => {
if (!err.length) {
isValidJSON(value) && onChange(JSON.parse(value));
} else {
console.log(err);
}
});
}
I think it is a normal behaviour of JSONEditor, according to docs:
{function} onChange()
Set a callback function triggered when the contents of the JSONEditor
change. This callback does not pass the changed contents, use get() or
getText() for that. Note that get() can throw an exception in mode
text, code, or preview, when the editor contains invalid JSON. Will
only be triggered on changes made by the user, not in case of
programmatic changes via the functions set, setText, update, or
updateText. See also callback functions onChangeJSON(json) and
onChangeText(jsonString). `

React top-level modal with dynamic props

I've architected my modals as suggested by Dan Abramov in this link. In short, he recommends rendering modals at the top level, and passing in the props at the same point that we dispatch an action to show the modal.
This works for the most part, but has one flaw that I cannot seem to overcome: The modal receives props only on the initial dispatch. If those props change at some point while the modal is open, those changes will not be reflected in the modal.
Here is my code:
MainScreen.js
const MainScreen = () => {
const { list, addListItem } = useListPicker()
const handleOpenModal = () => {
dispatch(modalShow('settingsModal', {
list,
addListItem,
}))
}
return (
/* ...some jsx */
<Button onPress={handleOpenModal} />
)
}
SettingsModal.js
const SettingsModal = ({ list, addListItem }) => {
return (
<List data={list} clickListItem={addListItem} />
)
}
useListPicker.js (where list is stored)
import produce from 'immer'
const initialState = {
list: [],
}
const reducer = (state, { type, ...action }) => {
switch (type) {
case 'ADD_LIST_ITEM':
return produce(state, (draft) => {
const { data } = action
draft.list.push(data)
})
default:
throw new Error('Called reducer without supported type.')
}
}
export default () => {
const [state, dispatch] = useReducer(reducer, initialState)
const addListItem = (data) => dispatch({ type: 'ADD_LIST_ITEM', data })
return {
list: state.list,
addListItem,
}
}
As we can see in SettingsModal, we can call the function passed down (addListItem) that will properly add to the list when called (this data lives in a custom hook). However, the SettingsModal has no way to get access to that updated list, since props are passed in only once: when we initially dispatch modalShow.
What is the best way to handle this issue? Would Dan's solution not be appropriate here? Before I refactored my modals to work in a similar way to his post, I rendered each modal in the relevant part of the tree. I refactored this because the same modal would appear elsewhere in the codebase.

memoized a closure and render callback with useCallback

let's I have a graphql mutation component, that I reuse in many places
const MarkAsViewed =({ type = 1, children }) => {
const markAsViewed = (commitMutation) => (type) => {
return commitMutation({
variables: { type }
});
};
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{
(commitMutation, { error, loading }) => children({
markAsViewed: markAsViewed(commitMutation)
})
}
</MarkAsViewedMutation>
);
};
however since markAsViewed is a closure function, it will always return different function with different ref which means different for react.
this makes the child component to have to do a useCallback like:
const alwaysSameRefFunc = useCallback(()=>{ markAsViewed(), []}
above works but creates 2 problems:
I get linter warning saying I should add markAsViewed as dependency blah blah. which I cannot, because it triggers infinite loop (since it's different ref every time)
everyone that uses <MarkAsViewed /> component will need to manually memoirzation
Ideally this is what I want, but it's an invalid code, because "markAsViewed" is not a react component and cannot have useCallback
const markAsViewed = (commitMutation) => useCallback((type) => {
return commitMutation({
variables: { type }
});
}, []);
any idea how can I solve the issue?
note: we are not ready to update Apollo version to have hoook yet
Does the following work?
const markAsViewed = commitMutation => type => {
return commitMutation({
variables: { type },
});
};
const MarkAsViewed = ({ type = 1, children }) => {
const fn = useCallback(
(commitMutation, { error, loading }) =>
children({
markAsViewed: markAsViewed(commitMutation),
}),
[children]
);
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{fn}
</MarkAsViewedMutation>
);
};
I'm not sure if that will work because it still depends on children, if that causes unnesesary renders then maybe post how MarkAsViewed component is rendered.

React native, cannot update during an existing state transition

I have a react native component. I got the error:
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
Code:
import....
class Register extends Component {
static navigationOptions = {
header: null,
};
async handleSubmit(values, customerCreate) {
const { email, password, firstName, lastName, phone } = values;
const input = { email, password, firstName, lastName, phone };
const customerCreateRes = await customerCreate({ variables: { input } });
const isCustomerCreated = !!customerCreateRes.data.customerCreate.customer.id;
if (isCustomerCreated) {
const isStoredCrediential = await storeCredential(email, password);
if (isStoredCrediential === true) {
// Store in redux
// Go to another screen
console.log('test');
}
}
}
render() {
return (
<Mutation mutation={CREATE_CUSTOMER_ACCOUNT}>
{
(customerCreate, { error, data }) => {
return (
<MainLayout
title="Create Account"
backButton
currentTab="profile"
navigation={this.props.navigation}
>
{ showError }
{ showSuccess }
<RegistrationForm
onSubmit={async (values) => this.handleSubmit(values, customerCreate)}
initialValues={this.props.initialValues}
/>
</MainLayout>
);
}
}
</Mutation>
);
}
}
const mapStateToProps = (state) => {
return {
....
};
};
export default connect(mapStateToProps)(Register);
CREATE_CUSTOMER_ACCOUNT is graphql:
import gql from 'graphql-tag';
export const CREATE_CUSTOMER_ACCOUNT = gql`
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
userErrors {
field
message
}
customer {
id
}
}
}
`;
More detail here
Who is using the handleSubmit?
There is a button in the form call the handleSubmit, when press.
is this syntax correct onPress={handleSubmit} ?
const PrimaryButton = ({ label, handleSubmit, disabled }) => {
let buttonStyle = styles.button;
if (!disabled) {
buttonStyle = { ...buttonStyle, ...styles.primaryButton };
}
return (
<Button block primary={!disabled} disabled={disabled} onPress={handleSubmit} style={buttonStyle}>
<Text style={styles.buttonText}>{label}</Text>
</Button>
);
};
export default PrimaryButton;
Update 1:
If I remove customerCreate (coming from graphql), the error disappears. It means the async await is actually correct, but I need the customerCreate
Did you check with following code ?
onSubmit={(values) => this.handleSubmit(values, customerCreate)}
If you are trying to add arguments to a handler in recompose, make sure that you're defining your arguments correctly in the handler.
Also can be you're accidentally calling the onSubmit method in your render method, you probably want to double check how your onSubmit in RegistrationForm component.
Also you might want to try one more thing, moving async handleSubmit(values, customerCreate) { to handleSubmit = async(values, customerCreate) =>;
If this doesn't work, please add up your RegistrationForm component as well.
Bottom line, unless your aren't setting state in render, this will not happen.
It turns out the async await syntax is correct. The full original code (not posted here) contains Toast component react-base. The other developer is able to tell me to remove it and the error is gone. Sometimes it is hard to debug.

Resources