I am doing a React project.
However, there is one question.
I try to manage the links in the page from one place.
For example,
if page have an external link, open a new window,
if it is a link of the same site,
it tries to move from the inside.
so,
class App extends Component {
componentDidMount() {
document.addEventListener('click', (e) => {
const target = e.target;
const aLink = target.closest('a');
if (aLink && aLink.getAttribute('href')) {
const href = aLink.getAttribute('href') || '';
if (href) {
if (isExternalLink(href)) {
window.open(href);
e.preventDefault();
} else if (isOurLink(href)) {
//
} else {
//
}
}
}
});
}
:
I want to do this.
Is this an anti-pattern?
Your react component while rendering anchor links can call a service, this service can check wether a url is internal or external. Using this information you can render using something like this...
public render(): JSX.Element {
var openInNewTab = someService.IsInternal(props.someUrl);
return (
<a href={props.someUrl} target={openInNewTab ? '_blank': ''}>
</a>
);
}
As already mentioned in comments, perhaps an HOC is more suited for this.
Related
I'm working on a community site where users can share and comment on websites (here it is)[https://beta.getkelvin.com/]. Websites can be filtered by category, but when a user goes into a specific page then backs out, the filter is removed.
Here's the process in steps:
User selects filter from drop down list, and this.state.topicSelected reflects the new value
User clicks a link to see a show page of an instance (a summary of a website), this.state.topicSelected reflects the correct value
User goes back to main page, and this.state.topicSelected is reverted back to 0
Instead of reverting back to 0, I want the value to still apply so the filter remains on the same category that the user selected before.
The problem seems to be that getInitialState is resetting the value of this.state.topicSelected back to 0 (as it's written in the code). When I try to put a dynamic value in 0's place, I get an undefined error.
Here's the getInitialState code:
var SitesArea = React.createClass({
getInitialState: function () {
return {
sortSelected: "most-recent",
topicSelected: 0 // need to overwrite with value of this.state.topicSelected when user selects filter
// I removed other attributes to clean it up for your sake
}
}
On here's the event:
onTopicClick: function (e) {
e.preventDefault();
this.setState({topicSelected: Number(e.target.value)});
if (Number(e.target.value) == 0) {
if (this.state.sortSelected == 'highest-score') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.score});
});
} else if (this.state.sortSelected == 'most-remarked') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.remarkable});
});
} else if (this.state.sortSelected == 'most-visited') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.visited});
});
} else if (this.state.sortSelected == 'most-recent') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.recent});
});
}
} else {
this.getSites(this.state.sortSelected, Number(e.target.value));
this.setState({sites: []}, function () {
this.setState({sites: this.state.filter_sites});
});
}
And lastly, the dropdown menu:
<select
value={this.state.topicSelected}
onChange={this.onTopicClick}
className="sort"
data-element>
{
// Add this option in the .then() when populating siteCategories()
[<option key='0'value='0'>Topics</option>].concat(
this.state.topics.map(function (topic) {
return (<option
key={topic.id}
value={topic.id}>{topic.name}</option>);
}))
}
How do I get it so that this.state.topicSelected doesn't get reset when a user goes back to the main page?
I think your main page is getting unmounted (destroyed) when the user navigates from the main page to the summary page. React creates a brand new instance of the main page component when they return. That reconstruction initializes the selected topic back to 0.
Here is a codepen that shows what might be happening. Foo == your main page, Bar == summary page. Click Increment topic a couple times, then navigate from Foo to Bar and back again. Console logs show that the Foo component gets unmounted when you navigate away, and reconstructed on return.
Note You seem to be using an older version of react, as evidenced by the presence of getInitialState and React.createClass. My pen follows the more modern approach of initializing state in the class constructor.
To solve the problem, you have to save that state outside the main component in something that isn't getting deleted and re-created as they navigate. Here are some choices for doing that
Expose an onTopicSelected event from your main page. The parent of the main page would pass in a function handler as a prop to hook that event. The handler should save the selected topic in the component state of the parent. This is kind of messy solution because the parent usually should not know or care about the internal state of its children
Stuff the selected topic into something that isn't react, like window.props. This is an ugly idea as well.
Learn about redux and plug it into your app.
Redux is the cleanest way to store this state, but it would require a bit of learning. I have implemented the first solution in this codepen if you want to see what it would look like.
The original codepen showing the problem is pasted below as a snippet. Switch to Full page mode if you try to run it here.
//jshint esnext:true
const Header = (props) => {
const {selectedPage, onNavigationChange} = props;
const disableFoo = selectedPage == 'foo'
const disableBar = selectedPage == 'bar';
return (
<div>
<h1>Header Component : {selectedPage}</h1>
<button disabled={disableFoo} onClick={() => onNavigationChange('foo')}>Foo page</button>
<button disabled={disableBar} onClick={() => onNavigationChange('bar')}>Bar page</button>
<hr/>
</div>
);
};
class Foo extends React.Component {
constructor(props) {
console.log('Foo constructor');
super(props);
this.state = {
topicSelected: 0
};
this.incrementTopic = this.incrementTopic.bind(this);
}
incrementTopic() {
const {topicSelected} = this.state
const newTopic = topicSelected + 1
console.log(`incrementing topic: old=${topicSelected} new=${newTopic}`)
this.setState({
topicSelected: newTopic
})
}
render() {
console.log('Foo::render');
return (<div>
<h2>The Foo content page : topicSelected={this.state.topicSelected}</h2>
<button onClick={this.incrementTopic}>Increment topic</button>
</div>
);
}
componentWillMount() {
console.log('Foo::componentWillMount');
}
componentDidMount() {
console.log('Foo::componentDidMount');
}
componentWillUnmount() {
console.log('Foo::componentWillUnmount');
}
componentWillUpdate() {
console.log('Foo::componentWillUpdate');
}
componentDidUpdate() {
console.log('Foo::componentDidUpdate');
}
}
const Bar = (props) => {
console.log('Bar::render');
return <h2>The Bar content page</h2>
}
const Body = (props) => {
console.log('Body::render');
const {selectedPage} = props;
if (selectedPage == 'foo') {
return <Foo/>;
} else if (selectedPage == 'bar') {
return <Bar/>
}
};
class Application extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPage: 'foo'
};
}
render() {
console.log('Application::render');
const {selectedPage} = this.state;
const navigationChange = (nextPage) => {
this.setState({
selectedPage: nextPage
})
}
return (
<div>
<Header selectedPage={selectedPage} onNavigationChange={navigationChange}/>
<Body selectedPage={selectedPage}/>
</div>
);
}
}
ReactDOM.render(<Application/>,
document.getElementById('main')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="main"></div>
The problem:
I want to have simple boolean flag that will be true when modal is opened and false when it is closed. And I want to update other components reactively depends on that flag
I hope there is a way to do it with relay only (Apollo has a solution for that). I don't want to connect redux of mobx or something like that (It is just simple boolean flag!).
What I already have:
It is possible to use commitLocalUpdate in order to modify your state.
Indeed I was able to create and modify my new flag like that:
class ModalComponent extends PureComponent {
componentDidMount() {
// Here I either create or update value if it exists
commitLocalUpdate(environment, (store) => {
if (!store.get('isModalOpened')) {
store.create('isModalOpened', 'Boolean').setValue(true);
} else {
store.get('isModalOpened').setValue(true);
}
});
}
componentWillUnmount() {
// Here I mark flag as false
commitLocalUpdate(environment, (store) => {
store.get('isModalOpened').setValue(false);
});
}
render() {
// This is just react component so you have full picture
return ReactDOM.createPortal(
<div
className={ styles.modalContainer }
>
dummy modal
</div>,
document.getElementById('modal'),
);
}
}
The challenge:
How to update other components reactively depends on that flag?
I can't fetch my flag like this:
const MyComponent = (props) => {
return (
<QueryRenderer
environment={ environment }
query={ graphql`
query MyComponentQuery {
isModalOpened
}`
} //PROBLEM IS HERE GraphQLParser: Unknown field `isModalOpened` on type `Query`
render={ ({ error, props: data, retry }) => {
return (
<div>
{data.isModalOpened}
<div/>
);
} }
/>);
};
Because Relay compiler throws me an error: GraphQLParser: Unknown field 'isModalOpened' on type 'Query'.
And the last problem:
How to avoid server request?
That information is stored on client side so there is no need for request.
I know there a few maybe similar questions like that and that. But they doesn't ask most difficult part of reactive update and answers are outdated.
If you need to store just one flag as you said, I recommend you to use React Context instead of Relay. You could do next:
Add Context to App component:
const ModalContext = React.createContext('modal');
export class App extends React.Component {
constructor() {
super();
this.state = {
isModalOpened: false
}
}
toggleModal = (value) => {
this.setState({
isModalOpened: value
})
};
getModalContextValue() {
return {
isModalOpened: this.state.isModalOpened,
toggleModal: this.toggleModal
}
}
render() {
return (
<ModalContext.Provider value={this.getModalContextValue()}>
//your child components
</ModalContext.Provider>
)
}
}
Get value from context everywhere you want:
const MyComponent = (props) => {
const { isModalOpened } = useContext(ModalContext);
return (
<div>
{isModalOpened}
</div>
);
};
If you will use this solution you will get rid of using additional libraries such as Relay and server requests.
I'm trying to add an 'active' class to a nav link when it is clicked.
in JQuery I could do something like:
$('.navlink').on('click', function() {
$("a").removeClass('active');
$(this).toggleClass('active');
});
This is the closet I've managed to get in React:
export class NavLink extends Component {
constructor(props) {
super(props);
this.toggleClass=this.toggleClass.bind(this);
this.state = {
isActive: 1,
}
}
toggleClass = () => {
this.setState({ isActive: this.props.id });
}
render() {
const { href, name, id } = this.props;
const classNames = this.state.isActive === id ? "nav-link active" : "nav-link";
return (
<a
className={classNames}
onClick={this.toggleClass.bind(this)} href={href}>{name}</a>
)
}
}
The problem is React only re-renders the clicked on link. So once a link has been given a class of active it won't be removed if a different link is clicked.
You would need to do something like this in terms of components:
NavLinkList
|--NavLink
|--NavLink
NavLinkList would be the one holding the activeLinkId in its state.
That activeLinkId would then be passed to each Link as prop so when it is changed the Links would re-render.
Also in that NavLinkList you would have the function which would change the activeLinkId on the Links onClick handler.
I've done something like this before. It is easier to do this through the parent component. I didn't have components for each of my links. I just set their classes before the return in the render. Here is a sample of the code I did that I hope can be helpful to you.
I did the same thing with the onClick and the state just held a selected which held a String representing the name of the nav item. I had a Stories, Comments, and Likes nav links. Their original class was just 'tab'.
render () {
let storiesClass = 'tab';
let commentsClass = 'tab';
let likesClass = 'tab';
if (this.state.selected === "Stories") {
storiesClass += 'Selected';
commentsClass = 'tab';
likesClass = 'tab';
} else if (this.state.selected === "Comments") {
storiesClass = 'tab';
commentsClass += 'Selected';
likesClass = 'tab';
} else {
storiesClass = 'tab';
commentsClass = 'tab';
likesClass += 'Selected';
}
return (
<button className={storiesClass} onClick={this.selectTab("Stories")}>...
I'm not sure if this is possible due to the way meteor works. I'm trying to figure out how to unsubscribe and subscribe to collections and have the data removed from mini mongo on the client side when the user clicks a button. The problem I have in the example below is that when a user clicks the handleButtonAllCompanies all the data is delivered to the client and then if they click handleButtonTop100 it doesn't resubscribe. So the data on the client side doesn't change. Is it possible to do this?
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
handleButtonAllCompanies(event) {
event.preventDefault();
Meteor.subscribe('companiesAll');
}
handleButtonTop100(event) {
event.preventDefault();
Meteor.subscribe('companiesTop100');
}
render() {
// console.log(1, this.props.companiesASX);
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('admin.handleButtonAllCompanies', function() {
return CompaniesASX.find({});
});
Meteor.publish('admin.handleButtonTop100', function() {
return CompaniesASX.find({}, { limit: 100});
});
This is definitely possible, but the way to do that is to fix your publication. You want to think MVC, i.e., separate as much as possible the data and mode from the way you are going to present it. This means that you should not maintain two publications of the same collection, for two specific purposes. Rather you want to reuse the same publication, but just change the parameters as needed.
Meteor.publish('companies', function(limit) {
if (limit) {
return CompaniesASX.find({}, { limit });
} else {
return CompaniesASX.find({});
}
});
Then you can define your button handlers as:
handleButtonAllCompanies(event) {
event.preventDefault();
Meteor.subscribe('companies');
}
handleButtonTop100(event) {
event.preventDefault();
Meteor.subscribe('companies', 100);
}
This way you are changing an existing subscription, rather than creating a new one.
I'm not yet super familiar with react in meteor. But in blaze you wouldn't even need to re-subscribe. You would just provide a reactive variable as the subscription parameter and whenever that would change, meteor would update the subscription reactively. The same may be possible in react, but I'm not sure how.
Ok, first thanks to #Christian Fritz, his suggestion got me onto the right track. I hope this helps someone else.
I didn't realise that subscriptions should be controlled within the Container component, not in the page component. By using Session.set and Session.get I'm able to update the Container component which updates the subscription.
This works (if there is a better or more meteor way please post) and I hope this helps others if they come across a similar problem.
Path CompaniesContainer.js
export default UploadCSVFileContainer = withTracker(({ match }) => {
const limit = Session.get('limit');
const companiesHandle = Meteor.subscribe('companies', limit);
const companiesLoading = !companiesHandle.ready();
const companies = Companies.find().fetch();
const companiesExists = !companiesLoading && !!companies;
return {
companiesLoading,
companies,
companiesExists,
showCompanies: companiesExists ? Companies.find().fetch() : []
};
})(UploadCSVFilePage);
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
handleButtonAllCompanies(event) {
event.preventDefault();
// mongodb limit value of 0 is equivalent to setting no limit
Session.set('limit', 0)
}
handleButtonTop100(event) {
event.preventDefault();
Session.set('limit', 100)
}
render() {
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('companies', function() {
if (limit || limit === 0) {
return Companies.find({}, { limit: limit });
}
});
Path CompaniesContainer.js
export default CompaniesContainer = withTracker(() => {
let companiesHandle; // or fire subscribe here if you want the data to be loaded initially
const getCompanies = (limit) => {
companiesHandle = Meteor.subscribe('companies', limit);
}
return {
getCompanies,
companiesLoading: !companiesHandle.ready(),
companies: Companies.find().fetch(),
};
})(CompaniesPage);
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
getCompanies(limit) {
this.props.getCompanies(limit);
}
handleButtonAllCompanies(event) {
event.preventDefault();
// mongodb limit value of 0 is equivalent to setting no limit
this.getCompanies(0);
}
handleButtonTop100(event) {
event.preventDefault();
this.getCompanies(100);
}
render() {
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('companies', function(limit) {
if (!limit) { limit = 0; }
return Companies.find({}, { limit: limit });
});
I'm trying to make a button that only redirects the user to a new page after validation is completed correctly.
Is there a way of doing something like this?
How to I get a Route to be activated inside of a class method?
import validator from './validator';
class Example {
constructor(props) {
super(props)
this.saveAndContinue = thos.saveAndContinue.bind(this)
}
saveAndContinue () {
var valid = validator.validate(this.props.form)
if (valid) {
axios.post('/path')
<Redirect to='/other_tab'>
} else {
validator.showErrors()
}
}
render() {
<button onClick={this.saveAndContinue}>Save and Continue</button>
}
}
As discussed you should have access to the history object via this.props.history as described here.
If you look into the push function this will redirect you to any route you need.
For example:
// Use push, replace, and go to navigate around.
this.props.history.push('/home', { some: 'state' })
https://reacttraining.com/react-router/web/api/history
Like Purgatory said you can do it without using <Redirect />, but otherwise you could make your component a stateful component and then do a conditional render like so
render() {
!this.state.redirect ?
<button onClick={this.saveAndContinue}>Save and Continue</button> :
<Redirect to='/other_tab'>
}
And let the saveAndContinue() change the component state.
saveAndContinue () {
var valid = validator.validate(this.props.form)
if (valid) {
axios.post('/path')
this.setState({redirect: true});
} else {
validator.showErrors()
}
}
When the state changes it would cause a re-render and this time the <Redirect /> would be rendered.
Note: I didn't actually run this code snippet, so it may contain (hopefully minor) errors.