I need to change the style of some child components of a react components. Something like this:
import React, { Component } from 'react';
class Parent extends Component {
onClickHandler = (event) => {
this.props.children[0].props.style.marginLeft = "-100%";
}
render() {
<div onClick={this.onClickHandler}>
{this.props.children}
</div>
}
}
export default Parent;
The error i'm getting is:
TypeError: Cannot add property marginLeft, object is not extensible
May you help me guys?
Thanks a lot !!
The error you are getting is because you cannot modify props, since those are immutable. A simpler approach can be done using plain CSS and simple state management.
With this technique, you need a state variable to know when to add the class modifier. That class modifier is in charge of overriding the styles of the child component.
The JS would look like this:
import React, { Component } from "react";
class Parent extends Component {
constructor() {
this.state = {
bigMargin: false
};
}
onClickHandler = (event) => {
event.preventDefault();
this.setState({ bigMargin: true });
};
render() {
return (
<div className={`parent-class ${bigMargin && 'big-margin'}`} onClick={this.onClickHandler}>
{this.props.children}
</div>
);
}
}
export default Parent;
And the CSS can be something as simple as this (or as complex as you may want)
.big-margin:first-child {
margin-left: -100%;
}
React props are immutable and you can't change them, they are read only and you can't add new properties.
This is done via Object.preventExtensions, Object.seal and Object.freeze.
To "fix" the error partialy you should define marginLeft in the first child of your Parent component
<Parent>
<p style={{marginLeft: '0'}}>1</p>
<p>2</p>
</Parent>
You will get now a new Error :
TypeError: "marginLeft" is read-only
Imagine having the ability to change props, and you pass the same prop to many children, one of them change it value, this will lead to unexpected behavior.
Try something like
Grab the element by its id on click
document.getElementById("demo").style.marginLeft = '-100px'
Or use react refs to grab the element
Related
Here is my parent component
import React, { Component } from 'react';
class Parent extends Component {
state={
myData: ""
}
getDataOne=(data)=>{
console.log(data)
this.setState({ myData: data });
}
render(){
return (
<div>
<ChildComponentOne onGetSearch={this.getDataOne} />
<ChildComponentTwo dataOne={this.props.location.state.searchData} dataTwo={this.state.myData} />
</div>
);
}
}
export default Parent;
This is how it goes.
I've initially gotten data as a prop from the Component that redirected to this . That
is the data called dataOne, I passed it to ChildComponentTwo.
Then, I'm now trying to pass another data gotten from ChildComponentOne to ChildComponentTwo with another prop name called dataOne but it's returning empty string when trying to access it from ChildComponentTwo through this.props.
The only data I could access is dataOne.
Where am I getting it wrong?
Trying to reproduce your issue in a sandbox and it seems you have an issue publishing your data from ComponentOne.
It seems to be working just fine: https://codesandbox.io/s/muddy-bush-rbfct?file=/src/App.js
I think the issue is with the time it renders the components. First, the React DOM renders the parent component, and then it'll render both the first and second children components at the same time, so the updated state won't be visible to the second child, because it's already created before the state variable is set. try creating the first child component before the return as follows.
import React, { Component } from 'react';
class Parent extends Component {
state={
myData: ""
}
getDataOne=(data)=>{
console.log(data)
this.setState({ myData: data });
}
render(){
const firstComponent = <ChildComponentOne onGetSearch={this.getDataOne} />
const stateValue = this.state.myData
return (
<div>
{firstComponent}
<ChildComponentTwo dataOne={this.props.location.state.searchData} dataTwo={stateValue} />
</div>
);
}
}
export default Parent;
rendering the first child before the return will give some time to update the state.
I am learning HOCs and keep reading the above quote, but I do not understand what it means. If my HOC adds a method to my consuming component, can I use that method in the render method like so? If not how would I do what I am trying to do here:
import React, { Component } from 'react';
import { withMyHOC } from '../with_my_component'
class MyComponent extends Component {
constructor(props) {
super(props);
}
render() {
const { methodFromHOC }= this.props;
const result = methodFromHOC(someArgument);
return (
<div >
{result}
</div>
)
}
}
export default withMyHOC(MyComponent );
When you say, do not use HOC within the render method, it means that you shouldn't create an instance of the component wrapped by HOC within the render method of another component. For example, if you have a App Component which uses MyComponent, it shouldn't be like below
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
}
render() {
const { methodFromHOC }= this.props;
const result = methodFromHOC(someArgument);
return (
<div >
{result}
</div>
)
}
}
export default MyComponent;
import { withMyHOC } from '../with_my_component'
export default class App extends React.Component {
render() {
const Wrap = withMyHOC(MyComponent);
return (
<div>
{/* Other Code */}
<Wrap />
</div>
)
}
}
Why you shouldn't use it like above is because everytime render method is called a new instance of the MyComponent is created wrapped by HOC called Wrap and hence everytime it be be mounted again instead of going by the natural lifecycle or React.
However if your HOC passes a function as props, you can use it within the render as long as it doens't cause a re-render again otherwise it will lead to a infinite loop.
Also its better to memoize functions which are called in render directly to avoid computation again and again
CodeSandbox Demo
A High Order Component is a function which returns a Component, not jsx. When wrapping a component with an hoc, you're not changing the returned value of your component, you're changing the signature itself. Consider the following hoc
const withFoo = Component => props =>{
return <Component {...props} foo='foo' />
}
withFoo is a function which takes a Component (not jsx) as argument and returns a component. You don't need to call an hoc from render because the values it injects are already inside props of the wrapped component.
An hoc tells how a wrapped component will look like, changes it's definition so the only place to use it is in the component definition itself. Calling an hoc inside render creates a new instance of that component on each render. It's the equivalent of
const Component = () =>{
const ChildComponent = () =>{
return <span> Child </span>
}
return <ChildComponent /> //You're declaring again on each render
}
Use your high order components like this
const Component = ({ foo }) => <div>{ foo }</div>
export default withFoo(Component)
Or
const Component = withFoo(({ foo }) => <div>{ foo }</div>)
I have the structure of components (nested) that seems like this:
Container
ComponentA
ComponentB
ComponentC(want to handle event here with state that lives on container)
Do I need to pass as props all the way from Container, ComponentA, ComponentB and finally ComponentC to have this handler? Or is there another way like using Context API?
I'm finding a bit hard to handle events with react.js vs vue.js/angular.js because of this.
I would recommend using either Context API (as you mentioned) or Higher Order Components (HoC)
Context Api is your data center. You put all the data and click events that your application needs here and then with "Consumer" method you fetch them in any component regardless of how nested it is. Here is a basic example:
context.js //in your src folder.
import React, { Component, createContext } from "react";
import { storeProducts } from "./data"; //imported the data from data.js
const ProductContext = createContext(); //created context object
class ProductProvider extends Component {
state = {
products: storeProducts,
};
render() {
return (
<ProductContext.Provider
//we pass the data via value prop. anything here is accessible
value={{
...this.state,
addToCart: this.addToCart //I wont use this in the example because it would
be very long code, I wanna show you that, we pass data and event handlers here!
}}
>
// allows all the components access the data provided here
{this.props.children},
</ProductContext.Provider>
);
}
}
const ProductConsumer = ProductContext.Consumer;
export { ProductProvider, ProductConsumer };
Now we set up our data center with .Consumer and .Provider methods so we can access
here via "ProductConsumer" in our components. Let's say you want to display all your products in your home page.
ProductList.js
import React, { Component } from "react";
import Product from "./Product";
import { ProductConsumer } from "../context";
class ProductList extends Component {
render() {
return (
<React.Fragment>
<div className="container">
<div className="row">
<ProductConsumer>
//we fetch data here, pass the value as an argument of the function
{value => {
return value.products.map(product => {
return <Product key={product.id} />;
});
}}
</ProductConsumer>
</div>
</div>
</React.Fragment>
);
}
}
export default ProductList;
This is the logic behind the Context Api. It sounds scary but if you know the logic it is very simple. Instead of creating your data and events handlers inside of each component and prop drilling which is a big headache, just put data and your event handlers here and orchestrate them.
I hope it helps.
This is probably something that I should know but I don't quite understand the behavior of my component when I pass a function without parentheses. Here's my component code.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import AppBar from 'material-ui/AppBar';
import LoginButton from './LoginButton';
import LogoutButton from './LogoutButton';
class Header extends Component {
renderButton() {
switch (this.props.auth) {
case null:
return
case false:
return <LoginButton />
default:
return <LogoutButton />
}
}
handleTitleClick() {
return(
<Link to={this.props.auth ? '/classes' : '/'}>
QueueMe
</Link>
);
}
render() {
const styles = {
title: {
cursor: 'pointer',
},
};
return(
<AppBar
title={<span style={styles.title}>QueueMe</span>}
onTitleClick={this.handleTitleClick()}
iconElementRight={this.renderButton()}
showMenuIconButton={false}
/>
);
}
}
/*
* #input: redux state
* Allows the component to access certain piece of the state as props
* Reducers determine the key in the state
*/
function mapStateToProps(state) {
return { auth: state.auth };
}
export default connect(mapStateToProps)(Header);
For my onTitleClick property in <AppBar>, I get the expected behavior when I pass it handleTitleClick() but when I pass it handleTitleClick and click it, I get an error that says Cannot read property 'auth' of undefined. What exactly is the difference here that causes the handleTitleClick not to be aware of the state?
Good question! There are a few things wrong going on here. Javascript this can be a real pain. The problem is that your functions are not bound.
When you write onTitleClick={this.handleTitleClick()} you are immediately invoking the function at compile time. When you pass it handleTitleClick when you are providing an unbound function, it has no this defined.
There are two potential solutions, you can either define
handleTitleClick = (event) =>
return(
<Link to={this.props.auth ? '/classes' : '/'}>
QueueMe
</Link>
);
}
This makes handleTitleClick an arrow function, arrow functions bind their this to the closure that they were created in.
If you don't like using the IIFE way, you can always use
constructor(props) {
super(props)
this.handleTitleClick = this.handleTitleClick.bind(this)
}
Check this out if you're still stuck.
https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56
You need to bind this to your component.
constructor(props){
super(props);
this.handleTitleClick = this.handleTitleClick.bind(this);
}
After this you can call without the parenthesis. Actually when you call with parenthesis you actually execute the function on render which may not actually the thing you want. You want to call the function only on click not on render. So use without parenthesis and bind your call in the constructor.
<AppBar
...
onTitleClick={this.handleTitleClick}
...
/>
At the moment, I'm converting the Markdown to HTML code using marked, then I replace some parts of it with React elements. This results in an array of HTML strings and React elements, which can be rendered indeed:
const prepareGuide = markdown => replaceToArray(
marked(markdown),
/<a href="SOME_SPECIAL_HREF".*?>(.*?)<\/a>/,
(match, label, slug) => <a href={`/${slug}`}>{label}</a>
)
const Guide = ({ guide }) =>
prepareGuide(guide.fields.text).map(
n => typeof n === 'object'
? n
: <span dangerouslySetInnerHTML={{ __html: n }} />
)
The problem with this, let's just call it workaround, is that every piece of HTML needs a wrapper element, like span (and uses dangerouslySetInnerHTML).
What I basically need is the ability to replace the rendered HTML elements with React components to add React functionality like Router links and other, custom elements.
Any other approaches?
Edit: The replaceToArray function I used is like String.prototype.replace, but returns an array (so any type can be returned)
Edit: Another approach I had was to render the HTML directly to the DOM (using dangerouslySetInnerHTML), and using the container element's ref to query all elements I want to replace. But, next problem: To render React components inside the HTML ref I have, I'd need another React root, which is possible, but unpractical, because I'd lose all the contexts (like Router), so I can't even properly use Router Links that way.
I was able to solve this as follows:
I kept using marked and dangerouslySetInnerHTML to directly set the HTML. Now, as described in the second approach, I used the ref to query the elements I want to replace. Now to be able to render React elements to the HTML, I just used the ReactDOM.render function.
The biggest problem with this was that components didn't have access to the app's context, since I now had multiple React roots. To solve this, I figured out that we can copy the context from one component to another: Is it possible to pass context into a component instantiated with ReactDOM.render?
So to be able to access the context in the component that renders the HTML, we need to set the component's contextTypes for the contexts we need to copy.
class Article extends Component {
static contextTypes = {
router: PropTypes.any
}
static propTypes = {
markdown: PropTypes.string
}
prepare(ref) {
const Provider = createContextProvider(this.context)
const links = Array.from(ref.querySelectorAll('a'))
links.forEach((link) => {
const span = document.createElement('span')
const { pathname } = url.parse(link.href)
const text = link.innerText
link.parentNode.replaceChild(span, link)
ReactDOM.render(
<Provider>
<Link to={pathname}>{text}</Link>
</Provider>,
span
)
})
}
render() {
return (
<article
ref={this.prepare}
dangerouslySetInnerHTML={{ __html: marked(this.props.markdown) }}
/>
)
}
}
The above code requires the snipped which I copied from the question linked above. The method I called prepare replaces specific HTML nodes with React roots.
function createContextProvider(context) {
class ContextProvider extends React.Component {
getChildContext() {
return context
}
render = () => this.props.children
static propTypes = { children: PropTypes.node }
}
ContextProvider.childContextTypes = {}
Object.keys(context).forEach(key => {
ContextProvider.childContextTypes[key] = PropTypes.any.isRequired
})
return ContextProvider
}
So we basically have a function that creates a Provider component. That function needs to be able to dynamically adapt to the required context types, that's why the loop sets them to be required.
If you simply want to have links work with React Router, you can render the markdown as usual with dangerouslySetInnerHTML then intercept internal link clicks to make them go through react-router.
Full example of loading .md externally, then catching links to process with react-router:
import React from "react"
import { withRouter } from 'react-router'
import catchLinks from 'catch-links'
import marked from "marked"
import styles from './styles.scss'
class ContentPage extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
markdown: '',
title: ''
}
}
componentDidMount() {
if (typeof window !== 'undefined') {
catchLinks(window, (href) => {
this.props.history.push(href);
});
}
const page = location.pathname.replace('/', '');
fetch(`/assets/content/${page}.md`)
.then(response => response.text())
.then(text => {
this.setState({ markdown: marked(text, {}) })
});
const title = page_titles[page] || capitalize(page);
if (title) {
document.title = title;
this.setState({title})
}
}
render() {
const {
markdown,
title
} = this.state;
return (
<div class={styles.contentPage}>
<div class={styles.pageTop}>
{title}
</div>
<div class={styles.page}>
<div class={styles.content} dangerouslySetInnerHTML={{__html: markdown}}></div>
</div>
</div>
);
}
}
export default withRouter(ContentPage);