I've been getting started with react-redux and finding it a very interesting way to simplify the front end code for an application using many objects that it acquires from a back end service where the objects need to be updated on the front end in approximately real time.
Using a container class largely automates the watching (which updates the objects in the store when they change). Here's an example:
const MethodListContainer = React.createClass({
render(){
return <MethodList {...this.props} />},
componentDidMount(){
this.fetchAndWatch('/list/method')},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(oId){
this.props.fetchObject(oId).then((obj) => {
this._unwatch = this.props.watchObject(oId);
return obj})}});
In trying to supply the rest of the application with as simple and clear separation as possible, I tried to supply an alternative 'connect' which would automatically supply an appropriate container thus:
const connect = (mapStateToProps, watchObjectId) => (component) => {
const ContainerComponent = React.createClass({
render(){
return <component {...this.props} />
},
componentDidMount(){
this.fetchAndWatch()},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(){
this.props.fetchObject(watchObjectId).then((obj) => {
this._unwatch = this.props.watchObject(watchObjectId);
return obj})}
});
return reduxConnect(mapStateToProps, actions)(ContainerComponent)
};
This is then used thus:
module.exports = connect(mapStateToProps, '/list/method')(MethodList)
However, component does not get rendered. The container is rendered except that the component does not get instantiated or rendered. The component renders (and updates) as expected if I don't pass it as a parameter and reference it directly instead.
No errors or warnings are generated.
What am I doing wrong?
This is my workaround rather than an explanation for the error:
In connect_obj.js:
"use strict";
import React from 'react';
import {connect} from 'react-redux';
import {actions} from 'redux/main';
import {gets} from 'redux/main';
import {isFunction, omit} from 'lodash';
/*
A connected wrapper that expects an oId property for an object it can get in the store.
It fetches the object and places it on the 'obj' property for its children (this prop will start as null
because the fetch is async). It also ensures that the object is watched while the children are mounted.
*/
const mapStateToProps = (state, ownProps) => ({obj: gets.getObject(state, ownProps.oId)});
function connectObj(Wrapped){
const HOC = React.createClass({
render(){
return <Wrapped {...this.props} />
},
componentDidMount(){
this.fetchAndWatch()},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(){
const {fetchObject, watchObject, oId} = this.props;
fetchObject(oId).then((obj) => {
this._unwatch = watchObject(oId);
return obj})}});
return connect(mapStateToProps, actions)(HOC)}
export default connectObj;
Then I can use it anywhere thus:
"use strict";
import React from 'react';
import connectObj from 'redux/connect_obj';
const Method = connectObj(React.createClass({
render(){
const {obj, oId} = this.props;
return (obj) ? <p>{obj.id}: {obj.name}/{obj.function}</p> : <p>Fetching {oId}</p>}}));
So connectObj achieves my goal of creating a project wide replacement for setting up the connect explicitly along with a container component to watch/unwatch the objects. This saves quite a lot of boiler plate and gives us a single place to maintain the setup and connection of the store to the components whose job is just to present the objects that may change over time (through updates from the service).
I still don't understand why my first attempt does not work and this workaround does not support injecting other state props (as all the actions are available there is no need to worry about the dispatches).
Try using a different variable name for the component parameter.
const connect = (mapStateToProps, watchObjectId) => (MyComponent) => {
const ContainerComponent = React.createClass({
render() {
return <MyComponent {...this.props} obj={this.state.obj} />
}
...
fetchAndWatch() {
fetchObject(watchObjectId).then(obj => {
this._unwatch = watchObject(watchObjectId);
this.setState({obj});
})
}
});
...
}
I think the problem might be because the component is in lower case (<component {...this.props} />). JSX treats lowercase elements as DOM element and capitalized as React element.
Edit:
If you need to access the obj data, you'll have to pass it as props to the component. Updated the code snippet
Related
I want to enumerate children props of a nested component without passing them over.
Let's take a look at this example (pseudo code)
# JSX
<Root>
<NodeWrapper />
</Root>
# NodeWrapper component
function NodeWrapper() {
return <InnerNode myPropName="myPropValue" />
}
# Root component
function Root({children}) {
// children.props > lists all NodeWrapper props
// how to get a hold of InnerNode props, so that Root can detect prop `myPropName`?
}
The only way I found so far is to pass myPropName to NodeWrapper. Is there a way to grab myPropName value from within Root component without passing it down from Root to InnerNode through NodeWrapper?
I understand InnerNode will be available only when NodeWrapper is rendered, that is not the case as Root is being rendered and InnerNode is not rendered yet (i.e., it is a component and not yet an instance).
I think this question hides some React concept I am missing.
EDIT: Please note that my question is not to avoid prop drilling. Prop drilling and contexts are techniques to pass data down the component tree. What I want to do is quite the opposite: read a nested component props from the Root. The usage of Root.children gives me only NodeWrapper props, but I do actually would like to get InnerNode props from within Root component.
I think you are trying to avoid props drilling that is passing props to children where it is not directly needed but it is needed inside some nested component. For this I would recommend to use Context it is great way to avoid prop drilling here how you can configure it
import './App.css';
import { useContext } from 'react';
// In the login Component
const InnerComponent = () => {
const authContext = useContext(MyContext);
const handleLogin = () => {
authContext.onAuthChange(true); // this will make the user login that change the value of auth to true
}
return (
<div>Login JSX</div>
)
}
const MyContext = React.createContext(null);
const NodeWrapper = () => <InnerComponent />
function App() {
const [auth, setAuth] = React.useState(true);
const handleAuthChange = (newAuthState) => {
setAuth(newAuthState);
}
return (
<MyContext.Provider value={{
auth,
onAuthChange: handleAuthChange
}}>
<NodeWrapper />
</MyContext.Provider>
);
}
export default App;
The React documentation says to pass the function defined in the Root component as a prop to the Child Component if you plan to update context from a nested component.
I have implemented the same:
import React from 'react';
const DataContext = React.createContext();
/**
* The App.
*/
export default class App extends React.Component {
constructor() {
super();
this.updateGreet = this.updateGreet.bind( this );
this.state = {
greet: '',
updateGreet: this.updateGreet
}
}
updateGreet() {
this.setState({
greet: 'Hello, User',
});
}
render() {
return (
<DataContext.Provider value={ this.state }>
<GreetButton />
<DisplayBox />
</DataContext.Provider>
)
}
}
/**
* Just a button element. On clicking it sets the state of `greet` variable.
*/
const GreetButton = () => {
return (
<DataContext.Consumer>
{
( { updateGreet } ) => {
return <button onClick={ updateGreet }>Greet</button>
}
}
</DataContext.Consumer>
)
}
/**
* Prints the value of `greet` variable between <h1> tags.
*/
const DisplayBox = () => {
return (
<DataContext.Consumer>
{
( { greet } ) => {
return <h1>{ greet }</h1>
}
}
</DataContext.Consumer>
)
}
It's a very simple React App I created for learning the Context API. What I'm trying to achieve is to define the updateGreet() method within the GreetButton component instead of defining it inside the App component since the function has nothing to do with the App component.
Another advantage I see is that if I choose to remove the GreetButton component altogether, then I need not keep track of all the methods it uses defined within another components.
Is there a way we can achieve this?
I would argue that the updateGreet method does have to do with App since it is manipulating App state.
I don't see this as a context-specific issue so much as the normal react practice of passing functions down to child components.
To accomplish your wish you could bind and pass the App's setState method to the provider and then implement updateGreet in the GreetButton component, but that would be an anti-pattern and I wouldn't recommend it.
When I am working with the Context API I typically define my context in a separate file and implement a custom provider to suit my needs, passing the related methods and properties down and consuming them throughout the tree as needed.
Essentially, implement what you have in App as its own Provider class GreetProvider. In the render method for GreetProvider simply pass the children through:
render() {
return (
<DataContext.Provider value={ this.state }>
{ this.props.children }
</DataContext.Provider>
)
}
Now, all of your greeting logic can live together at the source, with the context. Use your new GreetProvider class in App and any of its children will be able to consume its methods.
I have some heavy forms that I'm dealing with. Thus, I'm trying to squeeze performance wherever I can find it. Recently I added the Why-did-you-render addon to get more insight on what might be slowing down my pages. I noticed that, for example, when I click on a checkbox component about all of my other components re-render. The justification is always the same. WDYR says
Re-rendered because of props changes: different functions with the
same name {prev onChangeHandler: ƒ} "!==" {next onChangeHandler: ƒ}
As much as possible, I try to respect best the best practices indications that I find. The callback functions that my component passes follow this pattern
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export function TopLevelComponent({props}){
const defaultData = {name: '', useMale: false, useFemale: false}
const [data, setData] = useState(defData);
const { t } = useTranslation();
const updateState = (_attr, _val) => {
const update = {};
update[_attr] = _val;
setData({ ...data, ...update });
}
const updateName = (_v) => updateState('name', _v);//Text input
const updateUseMale = (_v) => updateState('useMale', _v);//checkbox
const updateUseFemale = (_v) => updateState('useFemale', _v);//checkbox
...
return <div>
...
<SomeInputComponent value={data.name} text={t('fullName')} onChangeHandler={updateName} />
<SomeCheckboxComponent value={data.useMale} onChangeHandler={updateUseMale} text={t('useMale')}/>
<SomeCheckboxComponent value={data.useFemale} onChangeHandler={updateUseFemale} text={t('useFemale')}/>
...
</div>
}
In an example like this one, altering any of the inputs (eg: Writing text in the text input or clicking one of the checkboxes) would cause the other 2 components to re-render with the justification presented above.
I guess that I could stop using functional components and utilize the shouldComponentUpdate() function, but functional components do present some advantages that I'd rather keep. How should I write my functions in such a way that interacting with one input does not force an update on another input?
The problem stems from the way you define your change handlers:
const updateName = (_v) => updateState('name', _v)
This line is called on each render and thus, every time your component is rendered, the prop has a new (albeit functionality-wise identical) value. The same holds for every other handler as well.
As an easy solution you can either upgrade your functional component to a fully fledged component and cache the handlers outside of the render function, or you can implement shouldComponentUpdate() in your child components.
You need to use memo for your child components to reduce renders
const SomeInputComponent = props => {
};
export default memo(SomeInputComponent);
// if it still causes rerender witout any prop change then you can use callback to allow or block render
e.f.
function arePropsEqual(prevProps, nextProps) {
return prevProps.name === nextProps.name; // use your logic to determine if props are same or not
}
export default memo(SomeInputComponent, arePropsEqual);
/* One reason for re-render is that `onChange` callback passed to child components is new on each parent render which causes child components to re-render even if you use `momo` because function is updated on each render so in order to fix this, you can use React hook `useCallback` to get the same function reference on each render.
So in you parent component, you need to do something like
*/
import { useCallback } from 'react';
const updateName = useCallback((_v) => updateState('name', _v), [])
You have to memoize parent function before pass to children, using useCallback for functional component or converting to class property if you use class.
export default class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick() {
console.log("click");
}
render() {
return (
<ChildComponent
onClick={ this.onClick }
/>
);
}
}
with useCallback:
Parent = () => {
const onClick = useCallback(
() => console.log('click'),
[]
);
return (
<ChildComponent
onClick={onClick}
/>
);
}
I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
The problem is coming from useContext. Whenever any value changes in your context, the component will re-render regardless of whether the value you're using has changed.
The solution is to create a HOC (i.e. withMyContext()) like so;
// MyContext.jsx
// exported for when you really want to use useContext();
export const MyContext = React.createContext();
// Provides values to the consumer
export function MyContextProvider(props){
const [state, setState] = React.useState();
const [otherValue, setOtherValue] = React.useState();
return <MyContext.Provider value={{state, setState, otherValue, setOtherValue}} {...props} />
}
// HOC that provides the value to the component passed.
export function withMyContext(Component){
<MyContext.Consumer>{(value) => <Component {...value} />}</MyContext.Consumer>
}
// MyComponent.jsx
const MyComponent = ({state}) => {
// do something with state
}
// compares stringified state to determine whether to render or not. This is
// specific to this component because we only care about when state changes,
// not otherValue
const areEqual = ({state:prev}, {state:next}) =>
JSON.stringify(prev) !== JSON.stringify(next)
// wraps the context and memo and will prevent unnecessary
// re-renders when otherValue changes in MyContext.
export default React.memo(withMyContext(MyComponent), areEqual)
Passing context as props instead of using it within render allows us to isolate the changing values we actually care about using areEqual. There's no way to make this comparison during render within useContext.
I would be a huge advocate for having a selector as a second argument similar to react-redux's new hooks useSelector. This would allow us to do something like
const state = useContext(MyContext, ({state}) => state);
Who's return value would only change when state changes, not the entire context.
But I'm just a dreamer.
This is probably the biggest argument I have right now for using react-redux over hooks for simple apps.
I'm trying to use the new React context to hold data about the logged-in user.
To do that, I create a context in a file called LoggedUserContext.js:
import React from 'react';
export const LoggedUserContext = React.createContext(
);
And sure enough, now I can get access to said context in other components using consumers, as I do here for example:
<LoggedUserContext.Consumer>
{user => (
(LoggedUserContext.name) ? LoggedUserContext.name : 'Choose a user or create one';
)}
</LoggedUserContext.Consumer>
But obviously, for this system to be useful I need to modify my context after login, so it can hold the user's data. I'm making a call to a REST API using axios, and I need to assign the retrieved data to my context:
axios.get(`${SERVER_URL}/users/${this.state.id}`).then(response => { /*What should I do here?*/});
I see no way to do that in React's documentation, but they even mention that holding info of a logged in user is one of the use cases they had in mind for contexts:
Context is designed to share data that can be considered “global” for
a tree of React components, such as the current authenticated user,
theme, or preferred language. For example, in the code below we
manually thread through a “theme” prop in order to style the Button
component:
So how can I do it?
In order to use Context, you need a Provider which takes a value, and that value could come from the state of the component and be updated
for instance
class App extends React.Component {
state = {
isAuth: false;
}
componentDidMount() {
APIcall().then((res) => { this.setState({isAuth: res}) // update isAuth })
}
render() {
<LoggedUserContext.Provider value={this.state.isAuth}>
<Child />
</LoggedUserContext.Provider>
}
}
The section about dynamic context explains it
Wrap your consuming component in a provider component:
import React from 'react';
const SERVER_URL = 'http://some_url.com';
const LoggedUserContext = React.createContext();
class App extends React.Component {
state = {
user: null,
id: 123
}
componentDidMount() {
axios.get(`${SERVER_URL}/users/${this.state.id}`).then(response => {
const user = response.data.user; // I can only guess here
this.setState({user});
});
}
render() {
return (
<LoggedUserContext.Provider value={this.state.user}>
<LoggedUserContext.Consumer>
{user => (
(user.name) ? user.name : 'Choose a user or create one';
)}
</LoggedUserContext.Consumer>
</LoggedUserContext.Provider>
);
}
}
I gave a complete example to make it even clearer (untested). See the docs for an example with better component composition.