material-ui : Extract color from theme - reactjs

I want to use a color from my material-ui theme inside a component like that :
const MyComponent = props => (
<UsersIcon color={currentTheme.primary1Color} />
)
So, my need is to extract a value from the current provided theme.
I found a working solution to solve this case, using context to retrieve the current theme :
const MyComponent = (props, {muiTheme}) => (
<UsersIcon color={muiTheme.palette.primary1Color} />
)
contextTypes = {
muiTheme: PropTypes.object.isRequired,
}
The React context is used "under the hood" by material-ui, so my solution is not future proof – the implementation of MUI can change –, is there any way to solve this in a proper (or recommended) way ?

You can access the theme variables with react hook or with higher-order component.
Example with hook:
//...
import { useTheme } from '#material-ui/core/styles';
const MyComponent = () => {
const theme = useTheme();
return <UsersIcon color={theme.palette.primary.main} />
}
Example with HOC:
//...
import { withTheme } from '#material-ui/core/styles';
const MyComponent = ({theme, ...other}) => {
return <UsersIcon color={theme.palette.primary.main} />
}
export default withTheme(MyComponent)
Don't forget to wrap root application component with ThemeProvider
Another method to mention is makeStyles for CSS-in-JS styling:
//...
import { makeStyles } from '#material-ui/core/styles'
const useStyles = makeStyles(theme => ({
icon: {
color: theme.palette.primary.main
}
}))
const MyComponent = () => {
const classes = useStyles()
return <UsersIcon className={classes.icon} />
}

Yes you have! using muiThemeable..
import muiThemeable from 'material-ui/styles/muiThemeable';
const MyComponent = props => (
<UsersIcon color={props.muiTheme.palette.primary1Color} />
)
export default muiThemeable()(MyComponent )
from material-ui docs

If your colors don't change at runtime, you can store these constants in a global object that gets used to initialize the theme as well as used in your custom components. This would allow you to not depend on context while keeping your code dry.

Related

Set MUI icon color correctly

I have a functional component which should set some default values for react-admin's BooleanField like this:
import ClearIcon from '#mui/icons-material/Clear'
import DoneIcon from '#mui/icons-material/Done'
import get from 'lodash/get'
import { BooleanField, BooleanFieldProps, useRecordContext } from 'react-admin'
import { SvgIcon } from '#mui/material'
const EventBooleanField = (props: BooleanFieldProps): JSX.Element => {
const { source, label, valueLabelTrue } = props
const record = useRecordContext(props)
const TrueIcon: typeof SvgIcon = () => {
return <DoneIcon color="success" />
}
TrueIcon.muiName = 'TrueIcon'
const FalseIcon: typeof SvgIcon = () => {
return <ClearIcon color="disabled" />
}
FalseIcon.muiName = 'FalseIcon'
return (
<BooleanField
sortable={false}
source={source}
valueLabelTrue={`${label}: ${get(record, valueLabelTrue)}`}
valueLabelFalse={`${label}: -`}
TrueIcon={TrueIcon}
FalseIcon={FalseIcon}
/>
)
}
export default EventBooleanField
It basically works, but the tooltip functionality seems broken as it does not display and in the console I can see:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `ForwardRef(Tooltip2)`
It seems that it does not like that I set the color prop of the mui icon in a functional component. How can I make this work?
Probably you need to use React.forwardRef to wrap your icons. Since that tooltip passes a ref to them. And you should spread props
const FalseIcon = React.forwardRef<any, any>((props, ref) => {
return <ClearIcon ref={ref} {...props} color="disabled" />
})
And something important: Never ever create a component inside another component, unless you do not keep its reference (with something like useCallback)!!!
Move your TrueIcon and FalseIcon outside of the EventBooleanField

Add location.pathname to React context

I'm doing some theming for a gatsby project I'm working on. I have the ThemeContext.Provider and ThemeContext.Consumer. The layouts differ depending on what page you're on. I was wondering if it's possible to store location.pathname in the ThemeProvider and have the path returned in my theme object as the page route changes. I want to pass the path to specific components to adjust the layout depending on the route. Thank you.
ThemeProvider:
import React, { useState, createContext } from 'react'
const defaultState = {
dark: false,
setDark: () => {},
}
export const ThemeContext = createContext(defaultState)
interface ThemeProviderProps {
children: any
}
export const ThemeProvider = (props: ThemeProviderProps) => {
const { children } = props.children
const [dark, setDarkTheme] = useState<boolean>(false)
const setDark = () => {
setDarkTheme(true)
}
return (
<ThemeContext.Provider
value={{
dark,
setDark,
}}
>
{children}
</ThemeContext.Provider>
)
}
In that case, you're storing unnecessary data in a context which make the possibility of bad rendering performance higher.
All you need is to use useLocation hook if you use Function Component or withRouter HOC if you Class Component to access the location object in your Component. just remember to use Router provider and you good to go.

Is it a correct way to extend Material-UI components (React / Typescript)

I'm trying to extend material-ui components as my own component with custom properties. I'm using reactjs w/ typescript.
The below code is my trial :
import React from 'react';
import clsx from 'clsx';
import { makeStyles } from '#material-ui/core';
import { Theme } from '#material-ui/core/styles/createMuiTheme';
import Tabs, { TabsProps } from '#material-ui/core/Tabs';
export interface Iprops extends TabsProps {
/* how to add a variant ? */
}
const useStyles = makeStyles((theme: Theme) => ({
root: {
// styles
}
}));
export const BasicTabs = (props: Iprops) => {
const classes = useStyles(props);
if (props.variant === 'test') {
return (
<Tabs
{...props}
className={clsx(classes.root, props.className)}
/>
);
}
return (
<Tabs {...props} />
);
};
So, what Im trying to do now is return a custom styled button when the variant is 'test'.
So, my first question is
1. How to add a new variant to the button?
the second question is
2. should I pass the children like
<Tabs {...props}>{props.children}</Tabs>
all the time whenever I extend a material-ui components?
import React from 'react'
import TextField, { StandardTextFieldProps } from '#material-ui/core/TextField'
/**
* Extend properties
*/
export interface PasswordProps extends StandardTextFieldProps {
custom: string
}
/**
* Password input
* #param props Properties
*/
export const PasswordField: React.FunctionComponent<PasswordProps> = (props) => {
props = Object.assign({ type: 'password' }, props)
return (
<TextField {...props}>
{props.children}
</TextField>
)
}
For your questions:
A. Extend the props, add variants to the interface you want.
B. Yes, always like this as suggested with React official 'composition model' https://reactjs.org/docs/composition-vs-inheritance.html

How to replace an HOC that strips props and assigns classnames with react hooks?

right now i have a higher-order-component that allows me juice up any component, it:
allows component it to take a color prop
passes the component a className with the color's css class
doesn't pass the color prop down to the component
it looks like this:
// withColor.js
import React from 'react'
import styles from './withColor.css'
import cn from 'classnames'
const withColor = TargetComponent => {
const WrappedComponent = props => {
// color is something like "black" or "red" and has a matching css class
const { className, color, ...rest } = props
const enhancedClassName = cn(className, {
[styles[`Color-${color}`]]: color,
})
return <TargetComponent className={enhancedClassName} {...rest} />
}
return WrappedComponent
}
example usage:
// box.js
import React from 'react'
import withColor from '../hocs/withColor'
const Box = props => <div data-box {...props} />
export default withColor(Box)
can i use react hooks to do this instead? what would it look like?
All you need is to write a custom hook that performs the above logic. Infact its not even a hook but a common function
const useWithColor = (props) => {
const { className, color, ...rest } = props
const enhancedClassName = cn(className, {
[styles[`Color-${color}`]]: color,
})
return {...rest, enhancedClassName};
}
and use it like
export default props => {
const dataProps = useWithColor(props);
return <div data-box {...dataProps} />
}

material-ui-next - Dynamically set palette color

I am using "material-ui": "^1.0.0-beta.33" for my project.
What I want to do is set primary palette color dynamically inside a react component (color will be fetched from some api).
Basically I want to override below:
const theme = createMuiTheme({
palette: {
primary: "some color from api"
},
})
Is there a way to set this in componentDidMount function of any component?
Reference: https://material-ui-next.com/
I created a component that uses MuiThemeProvider and wrap my entire app around that component. Below is the structure of the component.
import React, {Component} from "react";
import {connect} from "react-redux";
import {withStyles} from 'material-ui/styles';
import * as colors from 'material-ui/colors';
import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles';
import { withRouter } from 'react-router-dom';
export class ThemeWrapperComponent extends Component {
constructor(props){
super(props);
}
render(){
return (
<MuiThemeProvider theme={createMuiTheme(
{
palette: {
primary: { main: **colorFromApi** },
}
)}>
<div>
{ this.props.children }
</div>
</MuiThemeProvider>
)
}
}
export const ThemeWrapper = withRouter(connect(mapStateToProps)(ThemeWrapperComponent));
Below is how I wrapped my app around this component:
<ThemeWrapper>
<div>
<Routes/>
</div>
</ThemeWrapper>
Now, whatever colour you are sending from the api gets applied to the whole theme. More customisation can be done based on requirement.
I'm doing exactly this. Even got it working with WebMIDI using MIDI controller sliders and knobs just for fun.
The basic strategy is to use createMuiTheme and ThemeProvider and to store the theme in your application store (context, state, redux), etc.
class ThemeManager extends React.Component {
getThemeJson = () => this.props.context.themeJson || defaultThemeJson
componentDidMount () {
const themeJson = this.getThemeJson()
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
render () {
const { children, context } = this.props
const theme = context.theme
return theme
? <ThemeProvider theme={theme}>{children}</ThemeProvider>
: children
}
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/ThemeManager.js
and then you simply update your application's state.
handleImport = themeStr => {
const themeJson = JSON.parse(themeStr)
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/plugins/theme/components/ImportExportPanel.js#L17

Resources