ReactJS Redux toolkit Component does not update, only after refreshing the page - reactjs

Im using Redux-toolkit (already using it for a while). When I reset my redux store, and load my website for the first time the SharedList component does not update if I update the store.
But the strange thing... after refreshing the page, everything works perfect!
Im using the [...array] and Object.assign({}) methods. Also console logged, and the objects are NOT the same.
this is the reducer: (Only updates the ReactJS component after I refresh my page)
CREATE_SHARED_LIST_ITEM_LOCAL: (
proxyState,
action: PayloadAction<{
active: boolean;
checked: boolean;
id: string;
text: string;
}>
) => {
const state = current(proxyState);
const [groupIndex, group] = getCurrentGroupIndex(state);
const { id, text, active, checked } = action.payload;
const listItemIndex = group.shared_list.findIndex((item) => {
return item.id === id;
});
const group_id =
proxyState.groups[groupIndex].shared_list[listItemIndex].group_id;
const sharedItem: SharedListItem = {
text,
id,
checked,
active,
group_id,
};
proxyState.groups[groupIndex].shared_list[listItemIndex] = Object.assign(
{},
sharedItem
);
},
This is the ReactJS component
const GroceryList: React.FC = () => {
const [update, setUpdate] = useState(false);
const handleUpdate = () => {};
const dispatch = useDispatch();
const listItems = useSelector(selectSharedList);
const { setTitle } = useContext(HeaderContext);
const editItem = async (
id: string,
checked: boolean,
text: string,
active: boolean
) => {
dispatch(MUTATE_SHARED_LIST_LOCAL({ id, text, active, checked }));
};
const fetchList = async () => {
await dispatch(QUERY_SHARED_LIST({}));
};
useEffect(() => {
setTitle("Lijst");
fetchList();
}, []);
const list = listItems.filter((item) => {
return item.active;
});
return (
<>
<Card
// style={{
// paddingBottom: 56,
// }}
>
<CardContent>
<List>
{list.length === 0 ? (
<Box textAlign="center" justifyContent="center">
{/* <Center> */}
<Box>
<EmptyCartIcon style={{ width: "45%" }} />
</Box>
{/* </Center> */}
<Typography>De gedeelte lijst is leeg</Typography>
</Box>
) : null}
{list.map((item, index) => {
const { checked, text, id, active } = item;
return (
<ListItem dense disablePadding key={"item-" + index}>
<Checkbox
checked={checked}
onClick={() => {
editItem(id, !checked, text, active);
}}
/>
<EditTextSharedListItem item={item} />
<Box>
<IconButton
onClick={() => {
editItem(id, checked, text, false);
}}
>
<DeleteOutlineRoundedIcon />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</CardContent>
</Card>
<AddSharedListItem />
</>
);
};
This is the selector
export const selectSharedList = (state: RootState) => {
const group = selectCurrentGroup(state);
return group.shared_list.filter((item) => {
return item.active;
});
};

Related

React memoize inside a map function

Here is my problem: I'm making in Ract a clone of Google Forms. The structure of the data in the Redux store is as follows:
sections: [
{
sectionTitle: 'first section',
questions: [
{
questionType: 'checkbox',
question: 'some question',
options: [
{
number: 1,
text: 'sometimes',
},
],
},
],
},
],
In the feature loading the main edig page, I render this questions in the following map:
{form.sections.map((section, index) => {
return (
<Box key={index}>
<Box mb={5}>
<SectionCard index={index} />
</Box>
{section.questions.map((question, questionIndex) => {
return (
<QuestionCard
key={questionIndex}
sectionIndex={index}
questionIndex={questionIndex}
/>
);
})}
</Box>
);
})}
Each component picks the values from the store with useSelector, like the question description, the options, question type.
QuestionCard:
const QuestionCard = ({ sectionIndex, questionIndex }) => {
const question = useSelector(
state => state.form.sections[sectionIndex].questions[questionIndex]
);
const [hasQuestionChanged, setHasQuestionChanged] = useState(false);
const dispatch = useDispatch();
const handleChangeQuestionValue = e => {
dispatch(
setQuestionDescription({
sectionNumber: sectionIndex,
questionNumber: questionIndex,
newValue: e.target.value,
})
);
setHasQuestionChanged(true);
};
return(
...
<CardContent>
<TextField
multiline
label={`Question ${questionIndex + 1}`}
value={question.question}
margin='normal'
variant='standard'
onChange={handleChangeQuestionValue}
fullWidth
/>
{question.questionType === 'multiple' &&
question.options.map((option, index) => {
return (
<RadioQuestion
key={index}
sectionIndex={sectionIndex}
questionIndex={questionIndex}
optionIndex={index}
/>
);
})}
...
)
RadioQuestion
const RadioQuestion = memo(({ sectionIndex, questionIndex, optionIndex }) => {
const theme = useTheme();
const dispatch = useDispatch();
const [selected, setSelected] = useState(false);
const option = useSelector(
state =>
state.form.sections[sectionIndex].questions[questionIndex].options[
optionIndex
]
);
const handleChangeOptionDescription = e => {
dispatch(
setQuestionOptionDescription({
sectionIndex: sectionIndex,
questionIndex: questionIndex,
optionIndex: optionIndex,
description: e.target.value,
})
);
// Update the state
const handleChangeOptionValue = e => {
setSelected(!selected);
};
return(
...
<FormControl mt='1rem'>
<RadioGroup>
<Box justifyContent='center' sx={{ alignItems: 'center' }}>
<FormControlLabel
labelPlacement='top'
value={option.number}
control={<Radio />}
checked={selected}
sx={{ mt: 3 }}
onClick={handleChangeOptionValue}
/>
<FormControl>
<TextField
fullWidth
label={`Option ${option.number}`}
value={option.text ? option.text : ''}
variant='standard'
margin='normal'
onChange={handleChangeOptionDescription}
/>
</FormControl>
</Box>
</RadioGroup>
</FormControl>
...
)
SectionCard
const SectionCard = ({ index }) => {
const isMobile = useMediaQuery('(max-width: 400px)');
const theme = useTheme();
const dispatch = useDispatch();
const [errorTitle, setErrorTitle] = useState();
const { sectionTitle } = useSelector(state => state.form.sections[index]);
const totalSections = useSelector(
state => state.form.computedSections.computed.totalSections
);
const handleChangeSectionTitle = e => {
dispatch(
changeSectionTitle({ sectionIndex: index, newValue: e.target.value })
);
};
return (
...
<FormControl>
<TextField
required
autoComplete='off'
sx={{ minWidth: isMobile ? 150 : 400 }}
label='Section Title'
variant='standard'
value={sectionTitle}
margin='normal'
color={theme.palette.secondary[100]}
onChange={handleChangeSectionTitle}
error={errorTitle}
/>
{errorTitle && (
<FormHelperText error>
The Section Title can't be empty
</FormHelperText>
)}
</FormControl>
...
)
The problem is that when I edit a question filed and dispatch the value it rerenders all the question cards, which makes typing a bit slow. If I try to memoize the QuestionCard or the SectionCard I get a TypeError:
const SectionCard = memo(({ index }) => {...})
>Uncaught TypeError: Component is not a function
How the component looks
As you can see, every time I change a value, 20 rerenders happen, so I need to only rerender que component that has changed and I can't find the way to do it. I believe that the root problem is that the state is mapping through the sections and questions, and returning a new state, making all the components rerender.
As another guess on the problem and its solution, I understand that I shoul use useSelector to memoize, but I am really lost since the data structured is so nested.
Thanks in advance
I tried memoizing the function that renders the Section and the Question, which gives an error, I tried to useMemo on the parent component ant render the memoized component instead of the original. I am not sure on how to handle to render only when that specific component has changed.

Nextjs - update another component when delete item from database (supabase)

I have a problem with update my parent component when I try delete item from database.
I create simply get items from my database:
const filter = useFilter((query) => query.eq("createDate", date), [date]);
const [{ data, fetching, error }] = useSelect("payments_subitems", {
filter,
});
and I return by simply Map function.
BUT, I create another component to delete items from my database:
<RemoveItem id={date} />
const supabaseClient = useSupabaseClient();
const removeAction = async () => {
try {
const { data, error } = await supabaseClient
.from("payments_items")
.delete()
.eq("date", id);
if (error) throw error;
} catch (error: any) {
alert(error.message);
}
};
and I return simple button:
<Button
className="w-full text-white bg-red-600 hover:bg-red-700"
onClick={() => removeAction()}
>
{title}
</Button>
Delete action works correctly... but when I delete an item- the parent component doesn't change the list of items in my accordion listing :(
Anybody can help me?
I try to use useEffect but doesn't work :/ I don't know what I'm doing wrong :(
My parent component:
export function GetPaymentsSubitems({ date }: GetPaymentsSubitemsProps) {
const filter = useFilter((query) => query.eq("createDate", date), [date]);
const [{ data, fetching, error }] = useSelect("payments_subitems", {
filter,
});
return (
<>
{data?.map(({ title, value }, index) => {
return (
<div key={index} className="flex justify-between">
<div>{title}</div>
<div>{value}</div>
</div>
);
})}
</>
);
}
export function GetPaymentsAccordion({
userId,
children,
}: GetPaymentsAccordionProps) {
const payments: any = useSelector<any>((state) => state.payments);
const countReduxPayments = payments.payments.length;
const filter = useFilter((query) => query.eq("author", userId), [userId]);
const [{ data, fetching, error }] = useSelect("payments_items", {
filter,
pause: !userId,
});
const [expanded, setExpanded] = useState<string | false>(false);
const handleChange =
(panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
setExpanded(isExpanded ? panel : false);
};
if (error)
return (
<Alert severity="error">
<AlertTitle>Błąd</AlertTitle>
{error.message}
</Alert>
);
if (fetching)
return (
<div className="flex items-center justify-center">
<CircularProgress />
</div>
);
if (data?.length === 0 && countReduxPayments === 0)
return (
<Alert severity="info">
<AlertTitle>Brak danych</AlertTitle>
Nie posiadasz żadnych płatności
</Alert>
);
return (
<>
{data?.map(({ date }, index) => {
return (
<React.Fragment key={index}>
<Accordion
expanded={expanded === date}
onChange={handleChange(date)}
key={index}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
{date}
</AccordionSummary>
<AccordionDetails>
<GetPaymentsSubitems date={date} />
<RemoveItem id={date} />
</AccordionDetails>
</Accordion>
{children && children}
</React.Fragment>
);
})}
</>
);
}
Thanks!
You can add reexecute param under useSelect
const filter = useFilter((query) => query.eq("createDate", date), [date]);
const [{ data, fetching, error }, reexecute] = useSelect("payments_subitems", {
filter,
});
And then add another prop with this function execution to refresh your data like below
<RemoveItem id={date} refresh={() => { reexecute() }}/>
And the final step is adding that refresh() prop into removeAction
const supabaseClient = useSupabaseClient();
const removeAction = async () => {
try {
const { data, error } = await supabaseClient
.from("payments_items")
.delete()
.eq("date", id);
refresh(); //refresh your data here
if (error) throw error;
} catch (error: any) {
alert(error.message);
}
};

Update Child state from Parent using Context in React

I have a few buttons and "view all" button. The individual buttons load the coresponding data of that index or will show all the data by clicking the "view all" button. Problem I am running into is when I click my "view all" button in the parent it's not updating the state in the child component. On mounting it works as normal but on event handler in the "view all" it doesn't update. Any thoughts on where I am going wrong here?
JS:
...
const Context = createContext(false);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ id, styles, discription }) => {
const { activeViewAll, handleChange } = useContext(Context);
const [toggleThisButton, setToggleThisButton] = useState();
const handleClick = () => {
setToggleThisButton((prev) => !prev);
handleChange(discription, !toggleThisButton);
};
return (
<>
<Avatar
className={toggleThisButton && !activeViewAll ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(toggleThisButton)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(false);
useEffect(() => {
setActiveViewAll(true);
setSelected([...data]);
}, []);
const handleChange = (val, action) => {
let newVal = [];
if (activeViewAll) {
selected.splice(0, 3);
setActiveViewAll(false);
}
if (action) {
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log("action", action);
setSelected(newVal);
};
const handleViewAll = () => {
console.log("all clicked");
setActiveViewAll(true);
setSelected([...data]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</Context.Provider>
);
}
Codesanbox:
https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117
Issue
The issue seems to be that you are mixing up the management of the boolean activeViewAll state with the selected state.
Solution
When activeViewAll is true, pass the data array as the selected prop value to the ToggleContainer component, otherwise pass what is actually selected, the selected state.
Simplify the handlers. The handleViewAll callback only toggles the view all state to true, and the handleChange callback toggles the view all state back to false and selects/deselects the data item.
function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]); // none selected b/c view all true
const [activeViewAll, setActiveViewAll] = useState(true); // initially view all
const handleChange = (val, action) => {
setActiveViewAll(false); // deselect view all
setSelected(selected => {
if (action) {
return [...selected, val];
} else {
return selected.filter(v => v !== val)
}
});
};
const handleViewAll = () => {
setActiveViewAll(true); // select view all
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected} // pass all data, or selected only
/>
</div>
</Context.Provider>
);
}
In the ToggleContainer don't use the array index as the React key since you are mutating the array. Use the element value since they are unique and changing the order/index doesn't affect the value.
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update
Since it is now understood that you want to not remember what was previously selected before toggling activeViewAll then when toggling true clear the selected state array. Instead of duplicating the selected state in the children components, pass the selected array in the context and computed a derived isSelected state. This maintains a single source of truth for what is selected and removes the need to "synchronize" state between components.
const ToggleItem = ({ id, styles, description }) => {
const { handleChange, selected } = useContext(Context);
const isSelected = selected.includes(description);
const handleClick = () => {
handleChange(description);
};
return (
<>
<Avatar
className={isSelected ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(isSelected)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update the handleChange component to take only the selected value and determine if it needs to add/remove the value.
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(true);
const handleChange = (val) => {
setActiveViewAll(false);
setSelected((selected) => {
if (selected.includes(val)) {
return selected.filter((v) => v !== val);
} else {
return [...selected, val];
}
});
};
const handleViewAll = () => {
setActiveViewAll(true);
setSelected([]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange, selected }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={d}>
<ToggleItem id={id} styles={classes} description={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected}
/>
</div>
</Context.Provider>
);
}

Search bar, <input/>

Hello everyone :D I need your advise/tip. Right now I have a APIDataTable component. It has its rows, columns and etc. This component is responsible to show/build data table on frontend with search bar in it above the table. I have an search bar, which is not functional right now. I want it to search data from data table. What should I start from? How can i make it perform search in Table. Thank you for any advise and tip <3
import React, { useEffect, useState } from "react";
import { plainToClassFromExist } from "class-transformer";
import { Pagination } from "../../models/Pagination";
import {
DataTable,
DataTableHead,
DataTableHeadCell,
DataTableBody,
DataTableRow,
DataTableCell,
} from "../DataTable";
import { request } from "../../api";
import "./index.css";
import { MenuSurface } from "../MenuSurface";
import { IconButton } from "../IconButton";
import { Checkbox } from "../Checkbox";
import { Dialog } from "../Dialog";
import { GridCell, GridRow } from "../Grid";
import { Button } from "../Button";
export class Column<T> {
label: string;
width?: number;
filter?: JSX.Element;
render: (row: T) => JSX.Element | string;
constructor(column: Partial<Column<T>>) {
Object.assign(this, column);
}
}
type APIDataTableProps<T> = {
apiPath?: string;
params?: string;
columns?: Column<T>[];
type: Function;
onRowClick?: (row: T) => void;
};
export const APIDataTable = <T extends object>({
apiPath,
params,
columns,
type,
onRowClick,
}: APIDataTableProps<T>) => {
const [data, setData] = useState<Pagination<T>>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
const [isLoading, setIsLoading] = useState(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const [isMenuSurFaceOpen, setMenuSurfaceOpen] = useState(false);
const [hiddenColumns, setHiddenColumns] = useState<number[]>(
JSON.parse(localStorage.getItem(`hiddenColumns-${apiPath + params}`)) || []
);
const fetchData = async () => {
const urlSearchParams = new URLSearchParams(params);
urlSearchParams.set("page", page.toString());
urlSearchParams.set("page_size", pageSize.toString());
const url = `${apiPath}?${urlSearchParams}`;
const response = await request(url);
const data = plainToClassFromExist(new Pagination<T>(type), response, {
excludeExtraneousValues: true,
});
setData(data);
setIsLoading(false);
};
useEffect(() => {
if (!!apiPath) {
setIsLoading(true);
fetchData();
}
}, [page, pageSize]);
const headCells = columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => (
<DataTableHeadCell key={column.label} width={column.width}>
{column.label}
</DataTableHeadCell>
));
const rows = data?.results?.map((row, index) => (
<DataTableRow
key={"row-" + index}
onClick={() => !!onRowClick && onRowClick(row)}
>
{columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => {
return (
<DataTableCell key={column.label} width={column.width}>
<div className="data-table-cell-text">{column.render(row)}</div>
</DataTableCell>
);
})}
</DataTableRow>
));
let uncheckedCheckboxes = hiddenColumns;
const onCheckboxChange = (index: number, value: boolean) => {
if (!value) {
uncheckedCheckboxes.push(index);
//setHiddenColumns(uncheckedCheckboxes);
} else {
const array = [...uncheckedCheckboxes];
array.splice(array.indexOf(index), 1);
uncheckedCheckboxes = array;
}
};
const [isOpen, setIsOpen] = useState(false);
return (
<div className="data-table-container">
<div className="search-test">
<div className="mdc-menu-surface--anchor">
<label
className="mdc-text-field mdc-text-field--filled mdc-text-field--no-label mdc-text-field--with-leading-icon mdc-text-field--with-trailing-icon"
htmlFor="input"
id="search-menu-surface"
>
<IconButton density={-1} icon="search" />
<input
className="mdc-text-field__input "
type="text"
placeholder="Поиск"
id="searchinput"
/>
<IconButton
density={-1}
icon="arrow_drop_down"
onClick={() => {
setMenuSurfaceOpen(true);
}}
/>
</label>
<MenuSurface
isOpen={isMenuSurFaceOpen}
onClose={() => setMenuSurfaceOpen(false)}
fullwidth
>
<div className="data-table-filters-container">
{columns.map(
(column) =>
!!column.filter && (
<div className="data-table-filter">
<div className="data-table-filter-label mdc-typography--subtitle1">
{column.label}
</div>
<div className="data-table-column-filter">
{column.filter}
</div>
</div>
// <GridRow>
// <GridCell span={3}>{column.label}</GridCell>
// <GridCell span={3}>{column.filter}</GridCell>
// </GridRow>
)
)}
{/* <GridCell span={2}> */}
{/* <Button label="Поиск" raised /> */}
{/* <Button
label="Отмена"
raised
onClick={() => {
setIsOpen(false);
}}
/> */}
{/* </GridCell> */}
</div>
</MenuSurface>
</div>
<IconButton
onClick={() => {
setDialogOpen(true);
}}
density={-1}
icon="settings"
/>
<Dialog
isOpen={isDialogOpen}
onOkClick={() => {
localStorage.setItem(
`hiddenColumns-${apiPath + params}`,
JSON.stringify(uncheckedCheckboxes)
);
setDialogOpen(false);
setHiddenColumns(uncheckedCheckboxes);
}}
onCloseClick={() => setDialogOpen(false)}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{columns.map((column, index) => (
<Checkbox
label={column.label}
onChange={(value) => onCheckboxChange(index, value)}
defaultChecked={!uncheckedCheckboxes.includes(index)}
/>
))}
</div>
</Dialog>
</div>
<DataTable
pagination={true}
count={data?.count}
rowsNumber={data?.results.length}
page={page}
next={data?.next}
previous={data?.previous}
isLoading={isLoading}
onNextClick={() => setPage(page + 1)}
onPreviosClick={() => setPage(page - 1)}
>
<DataTableHead>{headCells}</DataTableHead>
<DataTableBody>{rows}</DataTableBody>
</DataTable>
</div>
);
};
I'm guessing that you want to search bar to effectively filter out rows that don't match. in this case what you want to do is add a filter to the search text (naturally you'll add a state for the search value, but it looks like you'll have that handled.
You'll add your filter here const rows = data?.results?.filter(...).map
You filter function will look something like this
const rows = data?.results.filter((row) => {
// In my own code if I have other filters I just make them return false
// if they don't match
if (
searchText &&
!(
// exact match example
row.field === searchText ||
// case-insensitive example
row.otherField?.toLowerCase().includes(searchText)
// can continue with '||' and matching any other field you want to search by
)
)
return false;
return true;
}).map(...)

Using React Hooks reference with Class and Function

I've been out of the React game for awhile. Come back and I'm trying to implement the Material UI library which has been rewritten with Hooks.
It seems to be extremely confusing + spagetti code in my eyes.
I simply want to reference a function so I can toggle the drawer, how can I do this?
// Old class
export default class DefaultContainer extends Component<ViewProps, any> {
render() {
return (
<View>
<MainAppBar
onPress={() => this.onMenuPressed()}
/>
{this.props.children}
<MainDrawer
ref={'drawer'}
/>
</View>
);
}
onMenuPressed = () => {
// TODO The bit that isn't working
(this.refs['drawer'] as Drawer).handleToggle()
}
}
Now the new material UI drawer
// New Drawer (3x more code now..)
const useStyles = makeStyles({
list: {
width: 280,
},
fullList: {
width: 'auto',
},
})
type Props = {
}
function MainDrawer(props: Props, ref: any) {
const classes = useStyles();
const [state, setState] = React.useState({
left: false,
});
const toggleDrawer = () => (
event: React.KeyboardEvent | React.MouseEvent,
) => {
if (
event.type === 'keydown' &&
((event as React.KeyboardEvent).key === 'Tab' ||
(event as React.KeyboardEvent).key === 'Shift')
) {
return;
}
setState({ ...state, left: true });
};
const inputRef = useRef();
useImperativeHandle(ref, () => {
toggleDrawer()
});
const sideList = () => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer()}
onKeyDown={toggleDrawer()}
>
<List>
<ListItem button key={'drawer_item'}>
<ListItemIcon><GroupIcon /></ListItemIcon>
<ListItemText primary={'Test Item'} />
</ListItem>
</List>
</div>
);
return (
<div>
<Button onClick={toggleDrawer()}>Open Left</Button>
<Drawer open={state.left} onClose={toggleDrawer()}>
{sideList()}
</Drawer>
</div>
);
}
export default forwardRef(MainDrawer);
I'm struggling to understand why you need to invoke a function from inside MainDrawer rather than just leveraging the use of props e.g.
Container
export default function DefaultContainer(props: ViewProps) {
const [drawerOpen, setDrawerOpen] = React.useState(false);
// assuming it's a toggle?
const toggleMenu = React.useCallback(() => setDrawerOpen(open => !open));
return (
<View>
<MainAppBar onPress={toggleMenu} />
{this.props.children}
<MainDrawer open={drawerOpen} />
</View>
)
}
MainDrawer
function MainDrawer(props: Props) {
const [open, setOpen] = React.useState(props.open);
...
const toggleDrawer = React.useCallback(() => setOpen(open => !open));
return (
<div>
<Button onClick={toggleDrawer}>Open Left</Button>
// use prop to determine whether drawer is open or closed
<Drawer open={open} onClose={toggleDrawer}>
{sideList()}
</Drawer>
</div>
);
}

Resources