I'm trying to create an appbar with tabs on it and then render it into my page using reactDOM, and it renders fine but clicking on a tab won't change tab for some reason.
What I tried so far:
printing the value state and checking that it actually changes (it does)
using the MUI example and seeing if it's a browser thing (it isn't)
Rendering the appbar, tabs, and tabpanels separately which led to more issues.
Some relevant code:
//this code is inside a component
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
//json_links is a list of jsons that provides user information
const ListOfTabs = json_links.map((link, index) => (
<Tab value={index} label={link['userName']} {...a11yProps({index})} />
))
//TabPanel is taken from the material-ui example on tabs
const ListOfTabPanels = json_links.map((link, index) => (
<TabPanel value={value} index={index}><Profile imagelist={link['images']} username={link['userName']}/></TabPanel>
))
let TabsElement = <div><AppBar id="tabsAppBar" position="static"><Tabs value={value} onChange={handleChange} aria-label="simple tabs example">{ListOfTabs}</Tabs></AppBar>{ListOfTabPanels}</div>
ReactDOM.render(TabsElement, document.getElementById("root"))
Expected behavior: users show in different tabs and a click switches users
Actual behavior: users show in different tabs and a click does nothing, it changes the state but the tab elements aren't "notified" of the change and don't respond
Related
so I have this code in material UI react js which is not updating the value properly or so I thought, to explain this further I have this code
import * as React from 'react';
import Tabs from '#mui/material/Tabs';
import Tab from '#mui/material/Tab';
export function Home() {
const [value, setValue] = React.useState('Pending');
const handleChange = (event, newValue) => {
setValue(newValue);
console.log(value);
};
return (
<div>
<>
<Tabs
value={value}
onChange={handleChange}
textColor="secondary"
indicatorColor="secondary"
aria-label="secondary tabs example"
>
<Tab value="Pending" label="Pending" />
<Tab value="Received" label="Received" />
<Tab value="Prepared" label="Prepared" />
<Tab value="Cancelled" label="Cancelled" />
</Tabs>
</>
</div>
);
}
If I run this code and click on the Tab with the label "Pending" the console.log won't be triggered. if I click again on the Tag with the label "Received" the console.log with displays "Pending" instead of "Received".
This happens all the time I thought when you set the value and console log it should show the latest value you selected.
sample output:
As you can see am currently selected the "Received" Tag but on the display its showing "Pending"
Is this how react js/Material UI behaves or am I just missing something here?
thanks
Like Dave Newton said in the comments, setting the state is asynchronous as mentioned in the React Docs here:
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
If you want to console.log the value, you can update your handler to log the new value instead
const handleChange = (event, newValue) => {
setValue(newValue);
console.log(newValue);
};
Or you can use useEffect outside the handler to always log value when it changes
useEffect(() => {
console.log(value);
}, [value]);
I have a list of "tasks" I'd like to map where each task has its own set of tabs. In the TabPanel, there is a component TaskOrg which fetches stuff from the backend (api calls that's significant) and displays the data in a DataGrid. I have no problem isolating each tab, meaning I can click on a tab and it behaves separately from all other tabs.
const [value, setValue] = React.useState({})
useEffect(() => {
if (provincialTasks.length > 0) {
let tabs = {}
provincialTasks.forEach(task => {
tabs[task.id] = false
})
setValue(tabs)
}
}, [provincialTasks])
function TabPanel(props) {
const {children, value, index, ...other} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{p: 3}}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
...
return (
{provincialSection.legalTasks?.map((provincialTask) => {
return (
<Tabs value={value[provincialTask.id]}>
<Tab value={`org-tab-${provincialTask.id}`} {...a11yProps(`org-tab-${provincialTask.id}`)} />
</Tabs>
<TabPanel value={value[provincialTask.id]} index={`org-tab-${provincialTask.id}`}>
<TaskOrg value={provincialTask.id} {...other} />
</TabPanel>
)
}}
)
The problem is that every time something changes in the parent, everything gets re-rendered, including tabs, of course. But on re-render, the TaskOrg also resets and fetches again the data from the backend and it displays that same data. I know there is useMemo but that only works for a single component. In this case, the provincialTasks objects can vary between 10-20... that's a lot of calls.
I tried passing an active prop to the TaskOrg component to only fetch data if I'm in the right tab, but it still doesn't prevent the re-render of that set of tabs (if I switch between tab for the same task).
Is there a way to prevent re-rendering on a set of child components, namely TaskOrg components should the parent decide it changed its state?
From the code you shared I suspect that you have that function called TabPanel that returns a component within your main component. When you do that, every time the main component updates, it updates the function as well, re-rendering the TabPanel. You should declare that function outside the main component to prevent unwanted re-renders
I have a comment component, which needs to be scrolled down to show the latest comment. But, in my case, if the user wants to scroll up, the component is forced automatically to scroll down.
Also, if the user enters a new comment, the component must come down if it is at the top.
I used useRef functionality to perform the above mentions scroll behavior:
const toBottom = () => {
scrollRef.current?.scrollIntoView();
}
useEffect( () => {
toBottom()
},[comments])
{comments?.map((comm, index) => (
<div>
Some Code
</div>
))}
<div ref={scrollRef} />
I am using react 17.0.2 and material ui 4.11.4
I want to customize the select element appearnce (like Chip component of material ui). For this purpose I am using Autocoomplete component which renders an input element. I have rendered Chip component below the input element.
I am also getting the ref in the renderInput callback function which I am ustlising to trigger input click from Chip component.
When I log the params.inputProps.ref.current I am indeed getting the input element but calling click function does not show the dropdown but just focus the input element. When I click the input directly then it shows the dropdown.
I have created a sandbox of this behaviour.
CodeSandBox
I would recommend using state as a controlled component.
export default function App() {
const classes = useStyles();
const statusDropdownInput = useRef(null);
let [val, setVal] = useState(false) // <- Store the open state
const handleStatusDropdownClick = (params,e) => {
setVal(!val) // <- Will toggle the dropdown
};
let inputElement = null
return (
<Autocomplete
className={classes.statusDropdown}
id="status"
open={val} // <- Control the input elements state here
options={["Option 1", "Option 2"]}
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<input type="text" {...params.inputProps} />
<Chip
size="small"
avatar={<DoneOutlinedIcon />}
label="Published"
clickable
onDelete={() => {
console.log(params.inputProps);
}}
onClick={(e) => handleStatusDropdownClick(params)}
deleteIcon={<ExpandMoreIcon />}
/>
</div>
)}
/>
);
}
I have a MUI Autocomplete inside a form from react hook form that works fine while filling the form, but when I want to show the form filled with fetched data, the MUI Autocomplete only displays the selected option after two renders.
I think it's something with useEffect and reset (from react hook form), because the Autocompletes whose options are static works fine, but the ones that I also have to fetch the options from my API only works properly after the second time the useEffect runs.
I can't reproduce a codesandbox because it's a large project that consumes a real api, but I can provide more information if needed. Thanks in advance if someone can help me with this.
The page where I choose an item to visualize inside the form:
const People: React.FC = () => {
const [show, setShow] = useState(false);
const [modalData, setModalData] = useState<PeopleProps>({} as PeopleProps);
async function showCustomer(id: string) {
await api
.get(`people/${id}`)
.then((response) => {
setModalData(response.data);
setShow(true);
})
.catch((error) => toast.error('Error')
)
}
return (
<>
{...} // there's a table here with items that onClick will fire showCustomer()
<Modal
data={modalData}
visible={show}
/>
</>
);
};
My form inside the Modal:
const Modal: React.FC<ModalProps> = ({data, visible}) => {
const [situations, setSituations] = useState<Options[]>([]);
const methods = useForm<PeopleProps>({defaultValues: data});
const {reset} = methods;
/* FETCH POSSIBLE SITUATIONS FROM API*/
useEffect(() => {
api
.get('situations')
.then((situation) => setSituations(situation.data.data))
.catch((error) => toast.error('Error'));
}, [visible]);
/* RESET FORM TO POPULATE WITH FETCHED DATA */
useEffect(() => reset(data), [visible]);
return (
<Dialog open={visible}>
<FormProvider {...methods}>
<DialogContent>
<ComboBox
name="situation_id"
label="Situação"
options={situations.map((item) => ({
id: item.id,
text: item.description
}))}
/>
</DialogContent>
</FormProvider>
</Dialog>
);
};
export default Modal;
ComboBox component:
const ComboBox: React.FC<ComboProps> = ({name, options, ...props}) => {
const {control, getValues} = useFormContext();
return (
<Controller
name={`${name}`}
control={control}
render={(props) => (
<Autocomplete
{...props}
options={options}
getOptionLabel={(option) => option.text}
getOptionSelected={(option, value) => option.id === value.id}
defaultValue={options.find(
(item) => item.id === getValues(`${name}`)
)}
renderInput={(params) => (
<TextField
variant="outlined"
{...props}
{...params}
/>
)}
onChange={(event, data) => {
props.field.onChange(data?.id);
}}
/>
)}
/>
);
};
export default ComboBox;
I think you simplify some things here:
render the <Modal /> component conditionally so you don't have to render it when you are not using it.
you shouldn't set the defaultValue for your <Autocomplete /> component as RHF will manage the state for you. So if you are resetting the form RHF will use that new value for this control.
it's much easier to just use one of the fetched options as the current/default value for the <Autocomplete /> - so instead of iterating over all your options every time a change is gonna happen (and passing situation_id as the value for this control), just find the default option after you fetched the situations and use this value to reset the form. In the CodeSandbox, i renamed your control from "situation_id" to "situation". This way you only have to map "situation_id" on the first render of <Modal /> and right before you would send the edited values to your api on save.
I made a small CodeSandbox trying to reproduce your use case, have a look:
mui#v4
mui#v5
Another important thing: you should use useFormContext only if you have deeply nested controls, otherwise just pass the control to your <ComboBox /> component. As with using FormProvider it could affect the performance of your app if the form gets bigger and complex. From the documentation:
React Hook Form's FormProvider is built upon React's Context API. It solves the problem where data is passed through the component tree without having to pass props down manually at every level. This also causes the component tree to trigger a re-render when React Hook Form triggers a state update