My personal opinion is that you should convert HeaderBar
to a function component. The reason that it needs to be a class right now is so you can use a ref to call a class method to modify the buttons. But this is not a good design to begin with. Refs should be avoided in cases where you can use props instead. In this case, you can pass down the buttons as a prop. I think the cleanest way to pass them down is by using the special children
prop.
Let’s create a BarButton
component to externalize the rendering of each button. This is basically your this.state.barButtons.forEach
callback, but we are moving it outside of the HeaderBar
component to keep our code flexible since the button doesn’t depend on the HeaderBar
(the header bar depends on the buttons).
What is a bar button and what does it need? It needs to have a label
text and a callback
function which we will call on click. I also allowed it to pass through any valid props of the material-ui Button
component. Note that we could have used children
instead of label
and that’s just down to personal preference.
You defined your ButtonState
as a callback which takes the HTMLButtonElement
as a prop, but none of the buttons shown here use this prop at all. But I did leave this be to keep your options open so that you have the possibility of using the button in the callback if you need it. Using e.currentTarget
instead of e.target
gets the right type for the element.
import Button, {ButtonProps as MaterialButtonProps} from "@material-ui/core/Button";
type ButtonState = (button: HTMLButtonElement) => void;
type BarButtonProps = {
label: string;
callback: ButtonState;
} & Omit<MaterialButtonProps, 'onClick'>
const BarButton = ({ label, callback, ...props }: BarButtonProps) => {
return (
<Button
color="inherit" // place first so it can be overwritten by props
onClick={(e) => callback(e.currentTarget)}
{...props}
>
{label}
</Button>
);
};
Our HeaderBar becomes a lot simpler. We need to render the home page button, and the rest of the buttons will come from props.childen
. If we define the type of HeaderBar
as FunctionComponent
that includes children
in the props (through a PropsWithChildren<T>
type which you can also use directly).
Since it’s now a function component, we can get the CSS classes from a material-ui hook.
const useStyles = makeStyles({
root: {
flexGrow: 1
},
menuButton: {
marginRight: 0
},
title: {
flexGrow: 1
}
});
const HeaderBar: FunctionComponent = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<HeaderMenu classes={classes} />
<Typography variant="h6" className={classes.title}>
<BarButton
callback={() => renderModule(<HomePage />)}
style={{ color: "white" }}
label="Sundt Memes"
/>
</Typography>
{children}
</Toolbar>
</AppBar>
</div>
);
};
Nothing up to this point has used state at all, BarButton
and HeaderBar
are purely for rendering. But we do need to determine whether to display “Log In” or “Log Out” based on the current login state.
I had said in my comment that the buttons would need to be stateful in the Layout
component, but in fact we can just use state to store an isLoggedIn
boolean
flag which we get from the response of AuthVerifier
(this could be made into its own hook). We decide which buttons to show based on this isLoggedIn
state.
I don’t know what this handle
prop is all about, so I haven’t optimized this at all. If this is tied to renderModule
, we could use a state in Layout
to store the contents, and pass down a setContents
method to be called by the buttons instead of renderModule
.
interface LayoutProp {
handle: ReactElement<any, any>;
}
export default function Layout(props: LayoutProp) {
// use a state to respond to an asynchronous response from AuthVerifier
// could start with a third state of null or undefined when we haven't gotten a response yet
const [isLoggedIn, setIsLoggedIn] = useState(false);
// You might want to put this inside a useEffect but I'm not sure when this
// needs to be re-run. On every re-render or just once?
AuthVerifier.verifySession((res) => setIsLoggedIn(res._isAuthenticated));
return (
<div>
<HeaderBar>
{isLoggedIn ? (
<BarButton
label="Log Out"
callback={() => new CookieManager("session").setCookie("")}
/>
) : (
<>
<BarButton
label="Log In"
callback={() => renderModule(<LogInPage />)}
/>
<BarButton
label="Sign Up"
callback={() => renderModule(<SignUpPage />)}
/>
</>
)}
</HeaderBar>
{props.handle}
</div>
);
}
I believe that this rewrite will allow you to use the material-ui styles that you want as well as improving code style, but I haven’t actually been able to test it since it relies on so many other pieces of your app. So let me know if you have issues.
CLICK HERE to find out more related problems solutions.