Material UI Snackbar autoHideDuration as null - reactjs

I am trying to implement a dynamic Material UI Snackbar that can either have a specific time to "auto-hide" or not. This information will come as props when I call my custom component.
About the autoHideDuration property, the documentation says:
The number of milliseconds to wait before automatically calling the onClose function. onClose should then set the state of the open prop to hide the Snackbar. This behavior is disabled by default with the null value.
I know that if I omit this parameter, my Snackbar will not auto-hide. But if I try to specify this parameter with a null value, I get a Type Error:
Type null is not assignable to type number
Here is the relevant part of the code I have:
const SnackbarComponent = (props: SnackbarProps) => {
const autoHideValue = props.stayOpen ? null : 4000;
return (
<Snackbar
open={ture}
autoHideDuration={autoHideValue} // Type Error on this line
onClose={handleClose}
/>
);
};
And I call it like this:
<SnackbarComponent stayOpen={true} />
The only solution that I could think of, is to have a conditional return. But this doesn't seem like the best way to do it:
const SnackbarComponent = (props: SnackbarProps) => {
if(props.stayOpen){
return (
<Snackbar
open={ture}
onClose={handleClose}
/>
);
} else {
return (
<Snackbar
open={ture}
autoHideDuration={4000}
onClose={handleClose}
/>
);
}
};
Any ideas on how to implement this?

const SnackbarComponent = (props: SnackbarProps) => {
const snackbarProps = {
open: true,
onClose: handleClose,
// this condition solves your problem
...(props.autoHideDuration && { props.autoHideDuration })
// or with default value
// ...(!props.stayOpen && { autoHideDuration: 4000 })
}
return <Snackbar {...snakbarProps} />;
};

Related

React, passed state is not changing in other component

I started with react and have a small problem
export default function TestComponent3({ typeId}) {
console.log('CHANGING HERE', { typeId});
const handleClick = () => {
console.log('NOT CHANGING HERE', { typeId});
console.log('NOT CHANGING HERE EITHER', typeId);
};
return (
<>
<Spectrum.Button variant="secondary" onClick={handleClick}>
CHANGING HERE {typeId}
</Spectrum.Button>
</>
);
}
In the other component the state of 'myProp' is changing by the UI dropdown.
Spectrum.Button content changes dynamically but console logs are stuck on default state option.
I probably messed something up and and It's easy fix.
EDIT://
Sibling with a dropdown
export default function TypeForm({ typeId, setTypeId }) {
return (
<div>
<Spectrum.Dropdown className="dropdown" placeholder="Choose type...">
<Spectrum.Menu onChange={setTypeId} slot="options">
{Types.map(type => { //types 0,1,2,3,4,5,6,7,8
return (
<Spectrum.MenuItem selected={typeId === type.id ? true : null} key={type.id} className="jsontest">
{type.name}
</Spectrum.MenuItem>
);
})}
</Spectrum.Menu>
</Spectrum.Dropdown>
)}
</div>
);
}
And Parent
export default function Form() {
const [typeId, setTypeId] = useState(0);
const setTypeIdFunc = e => {
setTypeId(e.target.selectedIndex);
};
return (
<TestComponent3 typeId={typeId} />
<TypeForm
typeId={typeId}
setTypeId={setTypeIdFunc}
/>
)
The props are passed down first from the parent component of TestComponent3 during the first render.
This means you would have to send handleClick from one "higher" component. Or use a shared state-like context provider.
I fixed it by changing Spectrum.Button to normal button.

Type properties so that they are mutually exclusive

How should I type my properties, so that either buttonLink or clickHandler, but not both, could be passed to my component at the same time?
export type ConfirmationProps = {
buttonLink?: string
clickHandler?: OnClick
}
export const Confirmation: FC<ConfirmationProps> = ({
buttonLink,
clickHandler,
}): JSX.Element => {
return (
{clickHandler && (
<Button onClick={clickHandler}>
Button
</Button>
)}
{buttonLink && (
<Link href={buttonLink}>
Link
</Link>
)}
)
}
Yes, it can be achieved by declaring possible variants and using never type:
export type ConfirmationProps = ({
buttonLink: string
clickHandler?: never
} | {
buttonLink?: never
clickHandler: OnClick
});
For your information, if there are other props which are common and acceptable for both variants then you can use the same solution, but firstly declare the common part and then use the & intersection literal:
export type ConfirmationProps = {
commonprop1: number
commonprop2: number
} & ({
buttonLink: string
clickHandler?: never
} | {
buttonLink?: never
clickHandler: OnClick
});

Make a component optionally controlled or uncontrolled

So I am trying to create this Collapse component either controlled or uncontrolled, optionally, depending on how its used, but that is quickly becoming a headache.
export const Collapse = ({
ExpandIcon = StyledExpandIcon,
CollapseIcon = StyledCollapseIcon,
expanded = false,
noButton = false,
...props
}) => {
const [isExpanded, setIsExpanded] = useState(expanded);
const handleSetExpanded = () => {
setIsExpanded((prevState) => !prevState);
};
return (
<>
{!noButton && (
<StyledIconButton onClick={handleSetExpanded}>
{isExpanded ? <CollapseIcon /> : <ExpandIcon />}
</StyledIconButton>
)}
<StyledCollapse in={expanded} {...props} />
</>
);
};
So I can't figure out how to make the useState optional, if the component is controlled, then its expanded state should be set somewhere outside of it, at the same time, both expanded and noButton should become required props, should either of them be supplied, so if I have suppplied only expanded then noButton should also be required, so that I remove the "default" button, same goes for first noButton.
I am pretty sure I can come up with some weird messed up logic and make it work, but what is the general way of doing something like this?
Your problem is your component being able to handle both controlled & uncontrolled values, right? This might not be on point to what use-case you might want. But in this case,
Add a variable to validate if your component is controlled/uncontrolled. You can check with const isControlled = expanded == null;
add an if statement to your handleSetExpanded to only call your setIsExpanded only if isControlled is false.
For your StyledCollapse && the inside of your StyledIconButton , you can pass a state of expanded || isExpanded. In that case, if expanded is null/undefined, then we'd be basing our value off of the state of your Collapse instead of the prop. Or better yet, can be isControlled ? expanded : isExpanded
Add warnings to your component (if not on prod)
This is what the code should look like.
export const Collapse = ({
ExpandIcon = StyledExpandIcon,
CollapseIcon = StyledCollapseIcon,
expanded = false,
noButton = false,
...props
}) => {
const [isExpanded, setIsExpanded] = useState(expanded)
const isControlled = expanded == null
const handleSetExpanded = () => {
if (!isControlled) {
setIsExpanded(prevState => !prevState)
}
}
if (isControlled && !noButton && process.env.NODE_ENV === 'production') {
React.useEffect(() => {
console.warning(
`You cannot use "expanded" prop without the "noButton" prop for the Collapse`,
)
}, [isControlled, noButton])
}
return (
<>
{!noButton && (
<StyledIconButton onClick={handleSetExpanded}>
{expanded || isExpanded ? <CollapseIcon /> : <ExpandIcon />}
</StyledIconButton>
)}
<StyledCollapse in={expanded || isExpanded} {...props} />
</>
)
}
Hope the code helps you or the bullet points at least guide you to what you want your code to look like! (Might I also point you to a Kent C Dodds talk that might guide you in the future)

Can not invoke possibly undefined object after checking with &&

This has probably been asked before (please refer me if true) but I've been trying to invoke a function after checking for its existence with &&
interface IProps {
primaryAction?: () => void;
}
const Comp: React.FC<IProps> = (props) => {
return (
<div>
{props.primaryAction && (
<Button onClick={() => props.primaryAction()}>
Click
</Button>
)}
</div>
);
};
The TS compiler complains:
Cannot invoke an object which is possibly 'undefined'.
Is there any way to work around this?
You did not check in the same scope where you use it. In theory, props could have had its content changed by the time your function runs.
Add this (terrible) code to your render function and you would get a runtime error on click:
setTimeout(() => props.primaryAction = undefined, 100)
This is the error that typescript is protecting you from. It's saying that it cannot guarantee the that your non-null check is still valid when you use that value.
This is why it's usually recommended to deconstruct props in functional components:
const Comp: React.FC<IProps> = ({ primaryAction }) => {
return (
<div>
{primaryAction && (
<Button onClick={() => primaryAction()}>
Click
</Button>
)}
</div>
);
};
Now you have direct reference to the value, and you know it can't be changed because no code outside could affect the assigned value without typescript noticing, because that would have to happen within this function.
Playground

React ES 6 Classes refs

I am very new in react native and I tried to refactor a source code from old react into react using ES 6 Class, but I got an error 'Cannot read property 'close' of undefined'. Can anyone help me why this.refs.drawer in closeDrawer is undefined?
closeDrawer = () => {
applicationActions.setDrawerStatus(false);
this.refs.drawer.close();
}
openDrawer = () => {
applicationActions.setDrawerStatus(true);
this.refs.drawer.open()
}
setDrawerState(value) {
this.setState({ isDrawerOpened: value });
}
render() {
return (
<Drawer ref="drawer"
type="static"
openDrawerOffset={DRAWER_OFFSET}
panOpenMask={.5}
onOpen={() => this.setDrawerState(true).bind(this)}
onClose={() => this.setDrawerState(false).bind(this)}
content={<DrawerScene closeDrawer={this.closeDrawer().bind(this)} />} >
<MainView
drawerStatus={this.isDrawerOpened}
closeDrawer={this.closeDrawer().bind(this)}
openDrawer={this.openDrawer().bind(this)}
/>
</Drawer>
);
}
Regards
EDIT I did not notice that you were using arrow functions in your component's member functions, so you do not need to bind them. There were some other issues, though
This is a binding issue. This should work:
closeDrawer = () => {
applicationActions.setDrawerStatus(false);
this.refs.drawer.close();
}
openDrawer = () => {
applicationActions.setDrawerStatus(true);
this.refs.drawer.open()
}
setDrawerState(value) {
this.setState({ isDrawerOpened: value });
}
render() {
return (
<Drawer ref="drawer"
type="static"
openDrawerOffset={DRAWER_OFFSET}
panOpenMask={.5}
onOpen={() => this.setDrawerState(true)}
onClose={() => this.setDrawerState(false)}
content={<DrawerScene closeDrawer={this.closeDrawer} />} >
<MainView
drawerStatus={this.isDrawerOpened}
closeDrawer={this.closeDrawer}
openDrawer={this.openDrawer}
/>
</Drawer>
);
}
The problem with your code is that you are applying bind to the result of a function call. For instance, when you do this.setDrawerState(true).bind(this), the function is called, returns the appropriate value, and then bind is applied to it. This usually would result in an error, but here you are also trying to access a ref that has not yet been set up (because before that happens all prop values have to be evaluated before passed to the new component, which is exactly the problem here, the function is called before the component is instantiated).
Just so you know a bit more about bind: it is a property of a function object, so you need to access it from the reference to that function (in this case, its name). The result of bind is a new function with the same behaviour of the original one, save for the new this value or any other parameters you pass.
Try to set ref like this instead of a string:
drawer = null;
closeDrawer = () => {
applicationActions.setDrawerStatus(false);
this.drawer.close();
}
openDrawer = () => {
applicationActions.setDrawerStatus(true);
this.drawer.open()
}
setDrawerState(value) {
this.setState({ isDrawerOpened: value });
}
render() {
return (
<Drawer ref={((component)=> this.drawer=component)}
type="static"
openDrawerOffset={DRAWER_OFFSET}
panOpenMask={.5}
onOpen={() => this.setDrawerState(true).bind(this)}
onClose={() => this.setDrawerState(false).bind(this)}
content={<DrawerScene closeDrawer={this.closeDrawer().bind(this)} />} >
<MainView
drawerStatus={this.isDrawerOpened}
closeDrawer={this.closeDrawer().bind(this)}
openDrawer={this.openDrawer().bind(this)}
/>
</Drawer>
);
}

Resources