I'm checking out React.js and trying to figure out how this library can work together with Isotope.js.
The documentation of React says that it plays nicely with other libraries, but using it with library that changes DOM on its own seems like no sense of using React.
Can someone explain to me, how can I take advantage of React in my webapp that uses Isotope.js as layout ?
My solution with useRef, useState and useEffect hooks. It also works with dynamically generated filter keys and items. The trick is to initialize Isotope after the component is mounted and call its "arrange" method every time the filter keyword changes.
Demo: https://codepen.io/ilovepku/pen/zYYKaYy
const IsotopeReact = () => {
// init one ref to store the future isotope object
const isotope = React.useRef()
// store the filter keyword in a state
const [filterKey, setFilterKey] = React.useState('*')
// initialize an Isotope object with configs
React.useEffect(() => {
isotope.current = new Isotope('.filter-container', {
itemSelector: '.filter-item',
layoutMode: 'fitRows',
})
// cleanup
return () => isotope.current.destroy()
}, [])
// handling filter key change
React.useEffect(() => {
filterKey === '*'
? isotope.current.arrange({filter: `*`})
: isotope.current.arrange({filter: `.${filterKey}`})
}, [filterKey])
const handleFilterKeyChange = key => () => setFilterKey(key)
return (
<>
<ul>
<li onClick={handleFilterKeyChange('*')}>Show Both</li>
<li onClick={handleFilterKeyChange('vege')}>Show Veges</li>
<li onClick={handleFilterKeyChange('fruit')}>Show Fruits</li>
</ul>
<hr />
<ul className="filter-container">
<div className="filter-item vege">
<span>Cucumber</span>
</div>
<div className="filter-item fruit">
<span>Apple</span>
</div>
<div className="filter-item fruit">
<span>Orange</span>
</div>
<div className="filter-item fruit vege">
<span>Tomato</span>
</div>
</ul>
</>
)
}
UPDATE (17/11/21):
TypeScript Demo: https://codesandbox.io/s/react-isotope-typescript-i9x5v
Don't forget to also add #types/isotope-layout as a dev dependency.
Here's a working version with Masonry, you should find it easy enough to port to Isotope (or use Masonry :)) http://jsfiddle.net/emy7x0dc/1/.
Here's the crux of the code that makes it work (and allow React to do its job).
var Grid = React.createClass({
displayName: 'Grid',
getInitialState: function(){
return {
masonry: null
}
},
// Wrapper to layout child elements passed in
render: function () {
var children = this.props.children;
return (
<div className="grid">
{children}
</div>
);
},
// When the DOM is rendered, let Masonry know what's changed
componentDidUpdate: function() {
if(this.state.masonry) {
this.state.masonry.reloadItems();
this.state.masonry.layout();
}
},
// Set up Masonry
componentDidMount: function() {
var container = this.getDOMNode();
if(!this.state.masonry) {
this.setState({
masonry: new Masonry( container )
});
} else {
this.state.masonry.reloadItems();
}
}
});
Here's an updated version of the above code posted by James:
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import Isotope from 'isotope-layout';
// Container for isotope grid
class ItemGrid extends PureComponent {
constructor(props) {
super(props);
this.state = { isotope: null };
}
render() {
return(
<div className="item-grid">
{this.props.children}
</div>
)
}
// set up isotope
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
if (!this.state.isotope) {
this.setState({
isotope: new Isotope( node )
});
} else {
this.state.isotope.reloadItems();
}
}
// update isotope layout
componentDidUpdate() {
if (this.state.isotope) {
this.state.isotope.reloadItems();
this.state.isotope.layout();
}
}
}
export default ItemGrid;
Usage:
Just pass the items you want to keep inside isotope into the ItemGrid component as children:
<ItemGrid>
{data.map(object => (
<Item key={object._id} name={object.name} imageUrl={object.imageUrl} />
))}
</ItemGrid>
Alternatives
If you can, consider using react-masonry-component.
You can manipulate the dom directly inside React. This permits to integrate existing JS libraries or for custom needs not handled well by React.
You can find an exemple here:
https://github.com/stample/gulp-browserify-react-phonegap-starter/blob/master/src/js/home/homeComponents.jsx#L22
And here's what it looks like:
The problem with integration of React and a library like Isotope is that you will end up having 2 different libraries trying to update the same dom subtree. As React work with diffs, it kind of assumes that it is alone modyfing the dom.
So the idea could be to create a React component that will render only one time, and will never update itself. You can ensure this with:
shouldComponentUpdate: function() {
return false;
}
With this you can:
Use React to generate your isotope item html elements (you can also create them without React)
On componentDidMount, initialize isotope on the dom node mounted by React
And that's all. Now React will never update this part of the dom again, and Isotope is free to manipulate it like it wants to without interfering with React.
In addition, as far as I understand, Isotope is not intented to be used with a dynamic list of items so it makes sense to have a React component that never updates.
Updated Hubert's answer to modern react with Hooks.
import React, { useEffect, useRef, useState } from 'react';
import Isotope from 'isotope-layout';
function IsoContainer (props) {
const isoRef = useRef();
const [isotope, setIsotope] = useState(null);
useEffect(() => {
if (isotope)
isotope.reloadItems();
else
setIsotope(new Isotope( isoRef.current ));
})
return (
<div ref={isoRef}>
{props.children}
</div>
)
}
export default IsoContainer
Edit: Coming back to Isotope after not using for a few months. Using a container component as above isn't the best practice for isotope. There are functions that exist on the isotope object that are needed, you also need to set the option in the new Isotope(ref, options) function, AND if you need to style the div, it's a little awkward to come back to this component.
It seems a better practice is to instead place the code within this component, into any component you are using isotope in. This way you a) have easy access to the isotope object, b) you can more easily style the container Div, and c) you can more easily pass and edit the isotope options.
You can of course keep the container as it is, though it becomes necessary to lift the state up, making this component a little unnecessary.
You need to create new Isotope object on componentDidMount and reload items on componentDidUpdate.
Use my mixin to figure it out :)
I got Isotope working in React by following Amith's quick tutorial at this link. The key was to address filtering within the onClick function:
class Parent extends Component {
constructor(props) {
super(props);
this.onFilterChange = this.onFilterChange.bind(this);
}
// Click Function
onFilterChange = (newFilter) => {
if (this.iso === undefined) {
this.iso = new Isotope('#filter-container', {
itemSelector: '.filter-item',
layoutMode: "fitRows"
});
}
if(newFilter === '*') {
this.iso.arrange({ filter: `*` });
} else {
this.iso.arrange({ filter: `.${newFilter}` });
}
}
render() {
return(
// Filter Buttons
<ul id="portfolio-flters">
<li data-filter="*" onClick={() => {this.onFilterChange("*")}}>All</li>
<li data-filter="filter-one" onClick={() => {this.onFilterChange("filter-one")}}>One</li>
<li data-filter="filter-two" onClick={() => {this.onFilterChange("filter-two")}}>Two</li>
</ul>
// Isotope Grid & items
<div id="filter-container">
<div className='filter-item filter-one'>
// Item Content
</div>
<div className='filter-item filter-two'>
// Item Content
</div>
</div>
)
}
}
It now works exactly like it did on my static jQuery site. If you want the filter buttons to change appearance when active you can simply update local state in the onFilterChange function and render the buttons based on that.
I don't know how but this doesn't work for me. But if I don't use the map function and use data manually this works.
import React, {useEffect, useState, useRef} from 'react';
import options from "./Options"
import ReactDom from 'react-dom'
import Isotope from 'isotope-layout';
import ItemGrid from "./ItemGrid";
const Home = () => {
const [question, setQuestion] = useState();
const [options, setOptions] = useState([]);
// store the isotope object in one state
const [isotope, setIsotope] = React.useState(null);
useEffect(() => {
Axios.get("http://localhost:8080/laravel/voting/public/api/question/3").then((res)=>{
console.log(res.data)
setQuestion(res.data.question);
setOptions(res.data.option)
});
}, []);
useEffect(() => {
setIsotope(
new Isotope(".filter-container", {
itemSelector: ".filter-item",
layoutMode: "vertical",
getSortData : {number: '.number parseInt'}
})
);
}, []);
const changeStateLevel = ()=>{
isotope.arrange({ sortBy: "number" });
}
return (
<>
<div className="row">
<div className="col-sm-7">
<div className="col-sm-14 mb-sm-5" >
<hr />
<ul className="filter-container">
{
options.map(object=>(
<div className="filter-item vege">
<p className="number">{object.vote}</p>
<span>Cucumber</span>
</div>
))
}
</ul>
</div>
</div>
<div className="col-sm-5">
</div>
<button className="btn btn-primary" onClick={changeStateLevel}> Change</button>
</div>
</>
);
}
export default Home;
Related
I am working on the following project https://github.com/codyc4321/react-udemy-course section 11 the videos app. The udemy course is found at https://www.udemy.com/course/react-redux/learn/lecture/12531374#overview.
The instructor is passing a callback down to multiple children and calling it in the lowest videoItem and the code is supposed to console log something out. I have no console log in my browser even though I've copied the code as written and double checked for spelling errors.
At the main level is App.js:
import React from 'react';
import youtube from '../apis/youtube';
import SearchBar from './SearchBar';
import VideoList from './VideoList';
class App extends React.Component {
state = {videos: [], selectedVideo: null};
onTermSubmit = async term => {
const response = await youtube.get('/search', {
params: {
q: term
}
});
// console.log(response.data.items);
this.setState({videos: response.data.items});
};
onVideoSelect = video => {
console.log('from the app', video);
}
render() {
return (
<div className="ui container">
<SearchBar onFormSubmit={this.onTermSubmit} />
<VideoList
onVideoSelect={this.onVideoSelect}
videos={this.state.videos} />
</div>
)
}
}
export default App;
videoList.js
import React from 'react';
import VideoItem from './VideoItem';
const VideoList = ({videos, onVideoSelect}) => {
const rendered_list = videos.map(video => {
return <VideoItem onVideoSelect={onVideoSelect} video={video} />
});
return <div className="ui relaxed divided list">{rendered_list}</div>;
};
export default VideoList;
the videoItem.js
import React from 'react';
import './VideoItem.css';
const VideoItem = ({video, onVideoSelect}) => {
return (
<div onClick={() => onVideoSelect(video)} className="item video-item">
<img
src={video.snippet.thumbnails.medium.url}
className="ui image"
/>
<div className="content">
<div className="header">{video.snippet.title}</div>
</div>
</div>
);
}
export default VideoItem;
The code that isn't running is
onVideoSelect = video => {
console.log('from the app', video);
}
My guess is that it has something to do with a key prop not being present in the map - I'm not super well versed with class components but I can't find anything else funky so maybe try adding a unique key prop in the map.
When rendering components through a map react needs help with assigning unique identifiers to keep track of re-renders etc for performance, that also applies to knowing which specific instance called a class method.
If you don't have a unique ID in the video prop you can use an index in a pinch, although ill advised, it can be found as the second parameter in the map function. The reason it's ill advised to use an index is if there are multiple children with the same index in the same rendering context, obviously the key parameter could be confused.
Okay-ish:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={index} onVideoSelect={onVideoSelect} video={video} />});
Better:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={video.id} onVideoSelect={onVideoSelect} video={video} />});
In my react project I am using react-window package to render nested lists. Each parent FixedSizeList row renders a component which uses another FixedSizeList. Parent List doesn't have more than 14 rows at the moment. But the child List may contain upto 2000 rows. Now my problem is, when I try to scroll through the parent List, all the child list items in the viewport seem to re rendering. This is a little bit problematic for me because in my child list item I am using d3js to draw bar chart with transition effect. So these unnecessary re rendering is giving a overall weird UI. Can anyone help me how can I stop these unnecessary renders.
Here is codesandbox link to a very simple example of my problem.
Please open the console log. After initial load the topmost log should be like this: initial console log.
Then if you clear the console and scroll the parent list, you will see log like this: console log after parent scrolling. Here you can see that the child list items of child list 0 is re rendering which is not needed for me.
Can anyone give me a solution that can stop these re rendering?
*P.S. I am not using memo since every row is updating the dom on its own.
Edit
I think this problem would solve if the parent list would stop propagating scroll event to child. I tried to add event.stopPropagation() and event.stopImmediatePropagation() in the parent list row but the output was the same as earlier.
We can use memo to get rid of components being re-rendered unnecessarily for same set of props. And use useCallback to prevent re-creation of a function and thus secure child components being re-rendered. Applying those, we can get this solution:
import "./styles.css";
import { FixedSizeList as List } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("rendering child list", parentIndex);
const InnerRow = useCallback(({ index, style }) => {
console.log("rendering child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
}, []);
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRow}
</List>
</div>
);
});
const Example = () => {
console.log("rendering parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
although the above code works fine, it can be optimized more if we use areEqual method from react-window as react memo dependency. And for more if we want to use other react hooks inside InnerRow component, we must add a middleware component of InnerRow. The full example is given below:
import { FixedSizeList as List, areEqual } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("mounting child list", parentIndex);
const data = new Array(15).fill(new Array(500).fill(1));
const InnerRowCallback = useCallback(
({ index, style }) => {
return <InnerRow index={index} style={style} />;
},
[data]
);
const InnerRow = ({ index, style }) => {
console.log("mounting child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
};
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRowCallback}
</List>
</div>
);
}, areEqual);
const Example = () => {
console.log("mounting parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
Here I am passing data array as useCallBack dependency because I want to re render the InnerRow component if data gets changed.
I'm making a dashboard that uses a common grid component. The grid has its own functionality separate and is used in other areas of the app. It needs to render a custom component within each grid item and has an active component that also renders a custom component, these custom components use functions from the Grids parent component, whatever is rendering it, below is how I do it current, but I'm pretty sure there is a better way of doing it.
Parent component that renders grid and passes down components and functions
import Grid from './common/grid'
class dashboard extends Component {
gridItemSpecificFunction() { console.log('success') }
activeFunction() { console.log('success again') }
render() {
return <Grid
CustomComponent={ CustomComponent }
ActiveComponent={ ActiveComponent }
activeFunctions={ {activeFunction} }
gridItemFunctions={ { gridItemSpecificFunction:this.gridItemSpecificFunction } }
/>
}
}
Grid that renders custom active and grid items based on data its passed
class Grid extends Component {
render() {
const {CustomComponent} = this.props
return (
<GridWrapper>
{ this.props.dynamicData.map( data => (
<GridItemWrapper>
<CustomComponent { ...data } functions={ this.props.gridItemFunctions } />
</GridItemWrapper>
) )
{ active && < ActiveComponent { ...activeData }
functions={ this.props.activeFunctions }/> }
</GridWrapper>
}
)
}
}
example of custom component that is using function passed through grid item
class CustomComponent extends Component {
render() {
const {gridItemSpecificFunction} = this.props.functions
return (
<div onClick={ gridItemSpecificFunction }>
{ this.props.text }
<div>
}
)
}
}
You actually doing great, of course there is another way to do this, probably is better just because become easier to modify, so you probably should use Context hook to get this done, so great packages based their functionalities in Context API Hook, so a great approach would be this
import React, { useContext, createContext, useMemo } from 'react';
const FunctionalityContext = createContext({}); // just leave it as empty state
// create a hook that return the context function
const useGridFunctions = () => {
const functions = useContext(FunctionalityContext);
return functions;
};
const CustomComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.gridItemSpecificFunction}>I am a custom component</button>
</div>
);
}
const ActiveComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.activeFunction}>I am a active component</button>
</div>
);
}
const ParentGrid = ({ functions }) => {
const functions = useMemo(() => functions, [functions]);
// the pass functions object to our context provider to get accesso through dom tree
return (
<FunctionalityContext.Provider value={functions}>
<Grid
CustomComponent={CustomComponent}
ActiveComponent={ActiveComponent}
/>
</FunctionalityContext.Provider>
);
}
As you can see you still keep your code almost the same, but you are adding a extra layer that will store the functionalities that will be used by components, so Context Api will help you to achieve this as you want.
Alright, I'm going to do my best to explain how my project is setup so that you can appropriately aid me on my quest to figure out how to approach this configuration.
I have a parent component that is a smart component. Through this component all my data from my store is being accessed.
class DashboardPage extends React.Component {
componentDidMount() {
this.props.getTips();
}
render() {
return (
<div>
<div className="row">
<div className="col-sm-7">
<ContentBox
title="The Scoop"
footerText="Submit a Story"
showSlider
content={<TipOfTheDay tips={this.props.tips} />}
/>
</div>
</div>
</div>
)
}
}
DashboardPage.propTypes = {
getTips: PropTypes.func
}
function mapStateToProps(state, ownProps) {
tips: state.tips
}
function mapDispatchToProps(dispatch) {
return {
getTips: () => { dispatch(tipActions.loadTips());} ## This hits tipActions and runs the `action creator`: loadTips(). Which returns all tips from api.
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
As you can see, I have included two dumb components inside my smart component, <ContentBox/> & <TipOfTheDay/>. On the dashboardPage there are about 7 <ContentBox/> components, each inheriting special a title for the header/footer and also being told whether or not to display the footer through the showSlider boolean. Here is what <ContentBox/> looks like:
import React, {PropTypes} from 'react';
import Footer from './ContentBoxFooter';
const ContentBox = ({title, footerText, showSlider, content}) => {
return (
<div style={styles.root} className="col-sm-12">
<div style={styles.header} className="row">
<h3 style={styles.header.title}>{title}</h3>
<span style={styles.header.arrow} />
</div>
{content}
<Footer footerText={footerText} showSlider={showSlider} />
</div>
);
};
ContentBox.propTypes = {
title: PropTypes.string,
footerText: PropTypes.string,
showSlider: PropTypes.bool,
content: PropTypes.object
};
export default ContentBox;
And here is the footer:
import React, {PropTypes} from 'react';
import styles from './contentBoxStyles';
import Previous from './svg/Previous';
import Next from './svg/Next';
import Add from './svg/Add';
import consts from '../../styles/consts';
const ContentBoxFooter = ({footerText, showSlider}) => {
if (footerText != undefined) {
return (
<div style={styles.footer} className="row">
{
showSlider ?
<div>
<Previous fillColor={consts.orange} height="20px" width="20px"/>
<span style={styles.bar}>|</span>
<Next fillColor={consts.orange} width="20px" height="20px"/>
</div> : <div style={styles.emptyArrow} />
}
<div style={styles.footer.link}>
<span style={styles.footer.link.text}>{footerText}</span>
<Add fillColor={consts.orange} height="24px" width="24px" />
</div>
</div>
);
} else {
return(null);
}
};
ContentBoxFooter.propTypes = {
footerText: PropTypes.string,
showSlider: PropTypes.bool
};
export default ContentBoxFooter;
Few! So here is where I need to add the onClick functionality. This functionality needs to be added to the <Previous/> & <Next/> component that is an SVG. What I am attempting to do is create a slider for the tips that I have pulled in. Obviously there will be <Footer/> components that will need the same functionality, but controlling different data other than the tips. Because I am new to React & Redux, I am not sure how I can perform this and not just do it, but do it in the 'Redux` way.
How do I get these two svg components that are nested within other dumb components that are dumb components, to perform onClick functions for specific data on the page? I hope this made sense. For more clarity, here is what I am doing with the <TipOfTheDay/> component:
const tipOfTheDay = ({tips}) => {
return (
<div style={styles.tipBody} className="row">
{
tips.map(function(tip, key) {
return (
<div key={key} className="myTips">
<h3 style={styles.tipBody.header}>{tip.title}</h3>
<p style={styles.tipBody.content}>{tip.content}</p>
</div>
);
})
}
</div>
);
};
tipOfTheDay.propTypes = {
tips: PropTypes.array.isRequired
};
export default tipOfTheDay;
Thank you for anytime you spend reading/responded/assisting with this question. I am a fairly new developer and this is also new technology to me.
I'm not sure how you've implemented your Next and Previous Components, but since you've using React-Redux, you can create extra Containers to wrap those components and pass in a Redux Action to them, e.g.:
// PreviousComponent.jsx
var Previous React.createClass({
propTypes: {
goToPrevious: React.PropTypes.func,
},
render: function() {
return (
<div onClick={this.props.goToPrevious}>
...
</div>
);
}
};
export default Previous;
//PreviousContainer.jsx
import ReactRedux from 'react-redux';
import Previous from './PreviousComponent';
var mapStateToProps = (state, props) => {
return {};
};
var mapDispatchToProps = (dispatch) => {
return {
goToPrevious: () => {
dispatch(Actions.goToPrevious());
},
}
};
var PreviousContainer = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps
)(Previous);
export default PreviousContainer;
By adding a container wrapper directly around your component, you can connect a redux action for going to the previous image/slide/whatever directly into your React component. Then, when you want to use the action in your ContentBoxFooter, you import the PreviousContainer and place it where you want the Previous component, e.g.:
//ContentBoxFooter.jsx
import PreviousContainer from './PreviousContainer'
const ContentBoxFooter = ({footerText, showSlider}) => {
if (footerText != undefined) {
return (
<div style={styles.footer} className="row">
{
showSlider ?
<div>
/*
* Add the PreviousContainer here where before you were only using your regular Previous component.
*/
<PreviousContainer fillColor={consts.orange} height="20px" width="20px"/>
<span style={styles.bar}>|</span>
<Next fillColor={consts.orange} width="20px" height="20px"/>
</div> : <div style={styles.emptyArrow} />
}
<div style={styles.footer.link}>
<span style={styles.footer.link.text}>{footerText}</span>
<Add fillColor={consts.orange} height="24px" width="24px" />
</div>
</div>
);
} else {
return(null);
}
};
ContentBoxFooter.propTypes = {
footerText: PropTypes.string,
showSlider: PropTypes.bool
};
By wrapping both the Next and Previous components in containers that pass actions into them, you can connect the Redux actions directly into your components without having to pass them from the root component of your application. Also, doing this allows you to isolate where certain actions are called. Your Previous button is probably the only component that would want to call a Previous action, so by placing it in a Container wrapper around the component, you're making sure that the Previous action is only used where it is needed.
Edit:
If you have to deal with multiple actions, it is better to define them at a higher level. In this case, since the ContentBox is the common breaking point, I would define separate Previous actions for each type of content box and pass them into each ContentBox instance:
var DashboardApp = React.createClass({
propTypes: {
TipsPrevious: React.PropTypes.function,
NewsPrevious: React.PropTypes.function,
},
render: function() {
<div>
<ContentBox
previousAction={this.props.TipsPrevious}
contentType='Tips'
/>
<ContentBox
previousAction={this.props.NewsPrevious}
contentType='News'
/>
...
</div>
},
});
Pass the actions down through the child components until you reach the Previous component and then attach the action to an 'onClick' handler on the Previous component.
The idea here behind this is that you want to limit the scope of parameters to the least amount of code possible. For example, if you added a profile component showing your user information on the page, you might want to add a container around that component and pass in the User-related information/actions without passing the information to the rest of your application. By doing this, it makes it easier to focus information where it is needed. It also helps you figure out where some change/action is taking place in your code if you have to fix a bug.
In the example above, if the Dashboard component is your root component, you'll just pass the Redux Actions into through a container wrapping it. However, if your dashboard component is a nested component itself, pass the actions into it through a custom container so that the actions aren't spread to code that don't need to see it:
<AppRoot>
<PageHeader/>
<DashboardContainer />
<PageSidebar />
<PageFooter />
</AppRoot>
I have a blog implementation based on ReactJS that I would like to integrate with AddThis. I have my social icons and I want to use them. So I'm looking for a way to integrate just the AddThis backend service.
I tried looking around but I was not able to find how to integrate AddThis to a ReactJS component.
I saw this somewhere and it uses a special namespace which to the best of my knowledge is not react friendly.
<div addthis:url='blog_url' addthis:title='blog_title' class="addthis_toolbox">
<a class="addthis_button_facebook">
<svg ... />
</a>
<a class="addthis_button_twitter">
<svg ... />
</a>
<a class="addthis_button_linkedin">
<svg ... />
</a>
<a class="addthis_button_reddit">
<svg ... />
</a>
</div>
<script type="text/javascript" src="http://s7.addthis.com/js/250/addthis_widget.js#pubid=xa-4fc9383e1ee05f1b"></script>
Also, I saw this JSFiddle with some information on it, but it is not using ReactJS and does not use custom icons.
Question: Is there any good documentation around AddThis + React?
In addition to the data attribute changes you should use the addthis.layers.refresh() method to dynamically refresh/load your AddThis components:
render() {
return (
<div className="addthis_inline_share_toolbox"
data-url={this.props.myurl}
data-title="Check out this URL"
>
</div>
);
}
Then in componentDidMount():
componentDidMount() {
addthis.layers.refresh();
}
EDIT: The above method is the initial approach i took and does initialise the add this widget however, the widget seems to not update the data-url when the prop is changed. even if i call addthis.layers.refresh(); again after a props update
Dynamic update solution:
In my render method:
// Don't use data attributes
<div className="addthis_inline_share_toolbox"></div>
Use the lifecycle methods:
componentDidMount() {
addthis.layers.refresh(); // important! init the add this widget
addthis.update('share', 'url', 'my-initial-url'); // update with initial prop value
}
componentDidUpdate() {
addthis.update('share', 'url', this.props.myurl); // update with prop value
}
componentWillReceiveProps(nextProps) {
addthis.update('share', 'url', nextProps.myurl); // update with prop value
}
Replace addthis:url and addthis:title with data-addthis-url and data-addthis-title.
I put this div in to display the addthis buttons.
<div className="addthis_inline_share_toolbox" data-url={ `http://[Your URL]` } data-title={ `[Your Title]` }></div>
But I also needed to load the javascript after the component mounted or the buttons never display. I assume if you add the javascript to your template that it's loading before the share_toolbox is loaded.
componentDidMount() {
setTimeout( () => {
var addthisScript = document.createElement('script');
addthisScript.setAttribute('src', 'http://s7.addthis.com/js/300/addthis_widget.js#pubid=[your id here]')
if (document.body) document.body.appendChild(addthisScript)
});
},
Here is how I did it:
Please, note that I'm using the inline share toolbox.
Thanks #Mark for addthis.update and to #jvoros for react-load-script
import React, { useEffect } from 'react';
import Script from 'react-load-script';
const AddThis = (props) => {
useEffect(() => {
if (window.addthis) {
window.addthis.update('share', 'url', props.url);
}
}, [props.url]);
const handleAddthisLoaded = () => {
window.addthis.init();
window.addthis.update('share', 'url', props.url);
};
return (
<>
<div className="addthis_inline_share_toolbox"></div>
<Script
url="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-xxxxxxxxxxxxxxxx"
onLoad={handleAddthisLoaded} />
</>
);
}
export default AddThis;
This was the only info I could find on implementing AddThis in a React app. Eventually helped me get a fix. I am posting my solution for anyone else who comes across this.
Using React Router and AddThis presented some challenges. The key was attaching the addThis javascript methods to window events and not React lifecycle events.
I used react-load-script to asynchronously load the script on the main page of my app, and implemented a callback to initialize the addThis widget and then set state to indicate if addThis was loaded. Then that state gets passed down to the component.
Partial code:
import * as LoadScript from 'react-load-script';
class App extends React.Component {
constructor(props) {
this.state = { addThisLoaded: false }
}
handleScriptLoad() {
this.setState({ addthisLoaded: true });
window.addthis.init();
}
render() {
return (
<LoadScript
url="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-xxxxxxxxxxxxxxxx"
onLoad={this.handleScriptLoad.bind(this)}
/>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/page/:id"
render={routeProps => (<Page {...routeProps} addThisLoaded={this.state.addThisLoaded} />)}
/>
</Switch>
);
}
}
Then in the component that implements the addThis widget I attached the window event listeners to the React lifecycle hooks. The addThisLoaded prop can be used to conditionally render the widget.
export default class Page extends React.Component<Props> {
componentDidMount() {
window.addEventListener('load', window.addthis.layers.refresh());
}
componentWillUnmount() {
window.removeEventListener('load', window.addthis.layers.refresh);
}
render() {
const page_url = `YOUR URL GOES HERE`;
const page_title = `YOUR TITLE GOES HERE`;
return (
<div>
{this.props.addThisLoaded === true && (
<div className="addthis_inline_share_toolbox" data-url={page_url} data-title={page_title} />
)}
</div>
);
}
}
If there is a better way to handle it I'd love to hear. The AddThis API docs are sparse. And the fact that it manipulates the DOM to get the desired behavior makes it tricky to incorporate with React Router.
Replace addthis:url with data-url