Changing material-ui theme on the fly --> Doesn't affect children - reactjs

I'm working on a react/redux-application where I'm using material-ui.
I am setting the theme in my CoreLayout-component (my top layer component) using context (in accordance to the documentation). This works as expected on initial load.
I want to be able to switch themes during runtime. When I select a new theme, my redux store gets updated and therefore triggers my components to update. The problem is that the children of my CoreLayout-component doesn't get affected - the first time! If I repeatedly change my theme (using a select-list that sends out a redux-action onChange) the children are updated. If a child component is located 2 layers down in my project hierarchy, it is updated after 2 action calls - so there is some issue with how the context is passed down.
My CoreLayout.js component
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import ThemeManager from 'material-ui/lib/styles/theme-manager';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class CoreLayout extends React.Component {
getChildContext() {
return {
muiTheme: ThemeManager.getMuiTheme(this.props.uiStatus.get("applicationTheme").toJS())
};
}
render () {
return (
<div className='page-container'>
{ this.props.children }
</div>
);
}
}
CoreLayout.propTypes = {
children: PropTypes.element
};
CoreLayout.childContextTypes = {
muiTheme: PropTypes.object
};
export default connect(mapStateToProps)(CoreLayout);
One of my child components (LeftNavigation.js)
import React from "react";
import { connect } from 'react-redux';
import { List, ListItem } from 'material-ui';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class LeftNavigation extends React.Component {
render () {
return (
<div className="left-pane-navigation">
<List subheader="My Subheader" >
<ListItem primaryText="Search" />
<ListItem primaryText="Performance Load" />
</List>
</div>
);
}
}
LeftNavigation.contextTypes = {
muiTheme: React.PropTypes.object
};
export default connect(mapStateToProps)(LeftNavigation);
I can access the theme located in context by this.context.muiTheme.
I can get the component to update the theme by using another instance of getChildContext() inside each child component, but I will have such a large number of components that I would very much like to avoid having to do that.
My CoreLayout component's getChildContext-method is called when I change theme and all my child components gets re-rendered as expected.
Any ideas?
Update: It works as expected on mobile devices (at least iOS)

You can use muiThemeProvider to avoid having to add getChildContext to any child component, the provider does this for you.
...
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import MyAwesomeReactComponent from './MyAwesomeReactComponent';
const App = () => (
<MuiThemeProvider muiTheme={getMuiTheme()}>
<MyAwesomeReactComponent />
</MuiThemeProvider>
)
...
More info in documentation.

Related

React Connected Component package without host having redux

I'm new to react and I'm trying to create a connected component (redux) for internal use in the company but I don't want to force the host app to have redux. I want to avoid having my component used like this:
<Provider store={store}><MyComponent /></Provider>
I thought in using something like Higher-Order Components and Contexts, but it is just not clicking.
Even creating my own provider would be acceptable, something like:
<MyComponentProvider><MyComponent /></MyComponentProvider>
Is it possible for a host application to use a Redux Component without having redux as a dependency?
It should be possible. The component can have its own redux store. Keep it as a complete black box.
Here is a demo, consider the package directory is your npm package and we install the redux, react-redux packages in the npm package, not your host or main project.
package/combobox.tsx:
import * as React from 'react';
import { useSelector } from 'react-redux';
export const Combobox = () => {
const state = useSelector((state) => state);
console.log('state: ', state);
return <div>combobox</div>;
};
we can wrap it in a special component that initializes the store in the constructor:
package/index.tsx:
import { Component } from 'react';
import * as React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { Combobox } from './combobox';
const reducer = (state = { counter: 1 }) => {
return state;
};
export default class ComboboxConnected extends Component {
store: ReturnType<typeof createStore>;
constructor(props) {
super(props);
this.store = createStore(reducer);
}
render() {
return (
<Provider store={this.store}>
<Combobox />
</Provider>
);
}
}
Now, each Combobox component instance has its own redux store and state.
Consumer side
App.tsx:
import * as React from 'react';
import Combobox from './package/index';
import './style.css';
export default function App() {
return (
<div>
<Combobox />
</div>
);
}
See Isolating Redux Sub-Apps
stackblitz

How to render a notification from a global function - React

I'm new to React and I am trying to utilize notistack for ReactJs and I would like to display the notification by calling a helper function but I'm not quite sure how to do that. Here is the standard code required to use the component:
App component:
import { SnackbarProvider } from 'notistack';
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
Component that displays the notification:
import { withSnackbar } from 'notistack';
class MyComponent extends Component {
handleNetworkRequest = () => {
fetchSomeData()
.then(() => this.props.enqueueSnackbar('Successfully fetched the data.'))
.catch(() => this.props.enqueueSnackbar('Failed fetching data.'));
};
render(){
//...
};
};
export default withSnackbar(MyComponent);
I would like to place the enqueueSnackbar('my notification message') inside a class or some kind of helper function so that I can call the helper function from anywhere in the react app to display a message without having to wrap the export of a component with withSnackbar(MyComponent);. How can this be done?
I would achieve this via Context API like so:
create a context object which holds the enqueueSnackbar function
Then pass it from the uppermost App comp or any other parent comp
Access it anywhere inside any child component and invoke it as needed
Some pseduo code:
// context.js
import React from 'react';
import { useSnackbar } from 'notistack';
const { enqueueSnackbar } = useSnackbar();
const snackbarContext = React.createContext({ enqueueSnackbar });
export default snackbarContext;
Then wrap a parent component in the tree with this context's provider like so:
//parent.js
import SnackbarContext from './context.js'
const App = () => {
return (
<SnackbarContext.Provider>
<SomeParentComponent />
</SnackbarContext.Provider>
);
}
Now it can be used inside a dummy child component like so:
// child.js
import React, {useContext} from 'react'
import SnackbarContext from './context.js'
const DummyChild = ()=>{
const {enqueueSnackbar} = useContext(SnackbarContext);
return (
<div>
<h1>Dummy Component with snackbar invocation</h1>
<button onClick={() => enqueueSnackbar('Wohoooo')}>Show Snackbar</button>
</div>
)
}

How should the new context api work with React Native navigator?

I created a multiscreen app using React Navigator following this example:
import {
createStackNavigator,
} from 'react-navigation';
const App = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
export default App;
Now I'd like to add a global configuration state using the new builtin context api, so I can have some common data which can be manipulated and displayed from multiple screens.
The problem is context apparently requires components having a common parent component, so that context can be passed down to child components.
How can I implement this using screens which do not share a common parent as far as I know, because they are managed by react navigator?
You can make it like this.
Create new file: GlobalContext.js
import React from 'react';
const GlobalContext = React.createContext({});
export class GlobalContextProvider extends React.Component {
state = {
isOnline: true
}
switchToOnline = () => {
this.setState({ isOnline: true });
}
switchToOffline = () => {
this.setState({ isOnline: false });
}
render () {
return (
<GlobalContext.Provider
value={{
...this.state,
switchToOnline: this.switchToOnline,
switchToOffline: this.switchToOffline
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
// create the consumer as higher order component
export const withGlobalContext = ChildComponent => props => (
<GlobalContext.Consumer>
{
context => <ChildComponent {...props} global={context} />
}
</GlobalContext.Consumer>
);
On index.js wrap your root component with context provider component.
<GlobalContextProvider>
<App />
</GlobalContextProvider>
Then on your screen HomeScreen.js use the consumer component like this.
import React from 'react';
import { View, Text } from 'react-native';
import { withGlobalContext } from './GlobalContext';
class HomeScreen extends React.Component {
render () {
return (
<View>
<Text>Is online: {this.props.global.isOnline}</Text>
</View>
)
}
}
export default withGlobalContext(HomeScreen);
You can also create multiple context provider to separate your concerns, and use the HOC consumer on the screen you want.
This answer takes in consideration react-navigation package.
You have to wrap your App component with the ContextProvider in order to have access to your context on both screens.
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import ProfileContextProvider from '../some/path/ProfileContextProvider'
const RootStack = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AppContainer = createAppContainer(RootStack)
const App = () => {
return (
<ProfileContextProvider>
<AppContainer />
</ProfileContextProvider>);
}
https://wix.github.io/react-native-navigation/docs/third-party-react-context/
As RNN screens are not part of the same component tree, updating the values in the shared context does not trigger a re-render across all screens. However you can still use the React.Context per RNN screen component tree.
If you need to trigger a re-render across all screens, there are many popular third party libraries such as MobX or Redux.

React Native HOC with nested react-navigation

I have a HOC used to pass react's Context as props to any component (a similar functionality and structure to redux):
import React from 'react';
import { GlobalContext } from './context';
export const globalContext = (mapStateToProps = state => ({ ...state })) => Children => props => (
<SignUpContext.Consumer>
{state => (<Children {...mapStateToProps(state)} {...props} />)}
</SignUpContext.Consumer>
);
This works as expected in normal components but when I try to use with a nested react navigator an error is thrown because cant access to the static router.
If I use either navigation or HOC It works but I can't manage to make both of them work
This would be a theoretical example of HOC and navigation:
import globalContext from './context'
import Screen from './screen';
import Screen2 from './screen2';
const MainStack = createBottomTabNavigator({
Screen: {screen: globalContext()(Screen)},
Screen2,
});
export default class Main extends Component {
static router = MainStack.router;
render() {
console.log(this.props.navigator); //does exist
return (
<View >
{ /*some component over all navigator*/}
<MainStack navigator={this.props.navigator} />
</View>
)
}
}
ok, that failed but while I was editing I realized that It was stupid to use the class so it worked just exporting the createBottomTabNavigator
Wrap each screen by HOC then pass it to navigation may be helped.
import globalContext from './context'
import Screen from './screen';
import Screen2 from './screen2';
const MainStack = createBottomTabNavigator({
globalContext()(Screen),
globalContext()(Screen2),
});
export default MainStack;

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