React Router 4 can not load new content on same component with <Link> - react-router-v4

I can't seem to trigger any other react component life cycle method other than render() when I click on a link that leads to a page that loads exactly the same component, even though the url is different. So here's my code
//index.js - the entry point
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Config from './Settings/Config';
import App from './Components/App';
const c = new Config();
render(
<BrowserRouter basename={c.routerBaseName}>
<App />
</BrowserRouter>
, document.getElementById('root'));
Here's my App JS
// Components/App.js
import React, {Component} from 'react';
import {Route} from 'react-router-dom';
import BlogEntry from './BlogEntry';
export default class App extends Component {
render() {
console.log('app');
return (
<div>
<Route exact path="/blog/:name" component={BlogEntry} />
</div>
)
}
}
And here is my BlogEntry.js
// Components/BlogEntry.js
import React from 'react';
import { Link } from 'react-router-dom';
export default class BlogEntry extends React.Component {
async componentDidMount() {
const [r1] = await Promise.all([
fetch(`http://api.myservice.com/${this.props.match.params.name}`)
]);
this.setState({content:await r1.json()});
console.log('fetch');
}
render() {
console.log('render');
if(!this.state) return <div></div>;
if(!this.state.content) return <div></div>;
const content = this.state.content;
return (
<div id="blog-entry" className="container">
<h1>{content.title}</h1>
<div dangerouslySetInnerHTML={{__html:content.content}}></div>
<div className="related-other">
<h2>Related Content</h2>
<ul>
<li><Link to="/blog/new-york-wins-the-contest">New York Wins the Contest!</Link></li>
<li><Link to="/blog/toronto-with-some-tasty-burgers">Toronto with Some Tasty Burgers</Link></li>
</ul>
</div>
</div>
);
}
}
So what happens is that when I click on the link for Toronto with Some Tasty Burgers or New York Wins the Contest! I see the url in my web browser address bar update accordingly. But my componentDidMount does not fire. And hence no new content is fetched or loaded.
React also won't let me put an onPress event handler to the <Link> object. And even if I did, managing the history state when browser clicks back button would be a nightmare if I were to create my own onpress event handler to load pages.
So my question is, how do I make it so that clicking on one of the links actually causes the component to fetch new data and redraw and also be part of the browser back button history?

I added this to my BlogEntry.js and everything works now:
componentWillReceiveProps(nextProps) {
this.props = nextProps;
}

I don't think your proposed solution, via componentWillReceiveProps (deprecated) is good enough. It's a hack.
Why don't you keep the route id in the state (as in /blog/:id).
Then something like this:
componentDidUpdate() {
const { match: { params: { id: postId } = {} } } = this.props;
if(this.state.postId !== postId) {
// fetch content
}
}

Related

Can't use ref to select swiper methods

I am trying to invoke .slideNext() & .slidePrev that come with swiper when the user presses the arrow keys.
I've managed to do this with querySelector like this:
const changeSlide = (event: KeyboardEvent) => {
const slides = document.querySelector('.swiper').swiper
if(event.key === 'ArrowLeft') {
slides.slidePrev();
} else if (event.key === 'ArrowRight') {
slides.slideNext();
}
}
However this method creates warnings and the usage of querySelector is not allowed in general for this project. So instead i wan't to use a ref to select the swiper but i've not yet succeeded at this.
This is what i tried:
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
// just some imports that are needed
const swiperRef = ref();
// swiper as ref
const changeSlide = (event: KeyboardEvent) => {
const slides = swiperRef.value.swiper
// instead of querySelector i use the ref.value
if(event.key === 'ArrowLeft') {
slides.slidePrev();
} else if (event.key === 'ArrowRight') {
slides.slideNext();
}
}
</script>
<template>
<swiper
ref="swiperRef"
:initial-slide="props.initialSlide"
:slides-per-view="1"
:space-between="0"
:modules="[Navigation]"
navigation
>
<swiper-slide> // left out the attributes since they are a mere distraction
<img/> // left out the attributes since they are a mere distraction
</swiper-slide>
</swiper>
</template>
The error i get from this code is:
From what I'm seeing in the source code, you can't access the swiper instance using ref on the swiper vue component because it's not exposed.
You have others way to do thought:
inside a child component of the <swiper> component, use the useSwiper() component.
on the parent component that instanciate the <swiper> component, use the #swiper event. It sends the swiper object once mounted:
<template>
<swiper #swiper="getRef" />
</template>
<script setup>
const swiper = ref(null)
function getRef (swiperInstance) {
swiper.value = swiperInstance
}
function next () {
swiper.value.slideNext() // should work
}
</script>
You don't have to use this method instead you can just import Keyboard like this:
import { Navigation } from 'swiper';
Add Keyboard to modules and attributes of <swiper> like this:
<swiper :modules="[Navigation, Keyboard]" keyboard />
And add the attribute to <swiper-slide> as well:
<swiper-slide keyboard />

Component stays visible when using props in composition API

I have a vue 3 app which display product information. The home component displays all available products. Each of them has a button which takes me to a component displaying information about said product called Single Box. It gets the product ID from the route params and then fetches additional information via graphql. It looks like this:
<template>
<BoxDisplay :box="box"></BoxDisplay>
</template>
<script>
import { useQuery, useResult } from '#vue/apollo-composable';
import { useRoute } from 'vue-router'
import singleBoxQuery from '../graphql/singleBox.query.gql'
import BoxDisplay from '../components/BoxDisplay'
export default {
components: {
BoxDisplay
},
setup() {
const route = useRoute();
const boxId = route.params.id
const {result } = useQuery(singleBoxQuery, {id: boxId})
const box = useResult(result, null)
return {
boxId,
box
}
}
}
</script>
The BoxDisplay component is then used to show the infos of the current box. There is going to be more going on this page later so the BoxDisplay is needed. It looks like this:
<template>
<div class="p-d-flex p-jc-center">
<div v-if="box">
<h1>{{box.Name}}</h1>
<div v-html="myhtml"></div>
</div>
<div v-else>
<ProgressSpinner />
</div>
</div>
</template>
<script>
export default {
props: {
box: {}
},
setup(props){
const myhtml = "<h1>hello</h1>" + props.box.Name
return {
myhtml
}
}
}
</script>
The problem is now, that as soon as I use props in setup() the component stay visible when I click back in my browser. It stays there until I refresh the page. What am I doing wrong?
Use ref and computed
https://v3.vuejs.org/guide/composition-api-introduction.html#reactive-variables-with-ref
```
import { ref, computed } from 'vue'
```
const myhtml = computed(() => '<h1>hello</h1>' + props.box.Name)
or
const myhtml = ref('<h1>hello</h1>' + props.box.Name)

onDismiss function in snackbar doesn't work in react-native-paper

I'm building mobile application with react-native and react-native-paper.
And I'm using SnackBar component in react-native-paper, and if I use SnackBar component directly, onDismiss function in SnackBar component works well. (It means the snackbar will disappear correctly)
But if I use my original component(like SnackBarComponent component) which uses SnackBar component provided react-native-paper, somehow, the snackbar will not disappear correctly.
This is my custom SnackBar Component and the code which calls my original SnackBar Component.
My original SnackBar Component
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Provider, Snackbar } from 'react-native-paper';
export default class SnackBarComponent extends Component {
constructor(props) {
super(props);
this.state = {
snackbarVisible: false
}
}
render() {
return(
<Provider>
<Snackbar
visible={this.props.snackbarVisible}
onDismiss={() => this.setState({ snackbarVisible: false })}
>
<Text>{this.props.snackbarText}</Text>
</Snackbar>
</Provider>
)
}
}
The code which calls SnackBarComponent(This is not whole code)
import SnackBarComponent from './components/SnackBarComponent';
:
handleShowSnackbar() {
this.setState({
snackbarVisible: true,
snackbarText: 'show snackbar'
})
}
:
<SnackBarComponent snackbarVisible={this.state.snackbarVisible} snackbarText={this.state.snackbarText}/>
:
You have a state containing snackbarVisible which is local to SnackBarComponent and it is initially false.
Then you have snackbarVisible in the parent component state where it's local to the parent component. It is not the same as snackbarVisible in SnackBarComponent.
In case you did not specifically defined a state in parent component containing snackbarVisible, please note that when you run setState method it will create snackbarVisible in the state if not found one.
When you are updating snackbarVisible(dismiss in this case) you have to update the one you defined here visible={this.props.snackbarVisible} which is containing the snackbarVisible in the parent component through the props. Which means you have to update the parent component's snackbarVisible. For that you can pass a callback to the SnackBarComponent and update the right value in the parent component. Here's an example:
//parent component
import SnackBarComponent from './components/SnackBarComponent';
:
handleShowSnackbar() {
this.setState({
snackbarVisible: true,
snackbarText: 'show snackbar'
})
}
//add a function to update the parents state
handleDismissSnackbar = () => {
this.setState({
snackbarVisible: false,
})
}
:
<SnackBarComponent snackbarVisible={this.state.snackbarVisible}
snackbarText={this.state.snackbarText}
dismissSnack={this.handleDismissSnackbar}/> //add here
Then the children component SnackBarComponent in this case as follows:
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Provider, Snackbar } from 'react-native-paper';
export default class SnackBarComponent extends Component {
//you dont need to maintain this local state anymore for this purpose
/*constructor(props) {
super(props);
this.state = {
snackbarVisible: false
}
}*/
render() {
return(
<Provider>
<Snackbar
visible={this.props.snackbarVisible}
onDismiss={() => this.props.dismissSnack()} //use that function here
>
<Text>{this.props.snackbarText}</Text>
</Snackbar>
</Provider>
)
}
}
Now when you press dismiss, it will call the handleDismissSnackbar in parent component by dismissSnack passed through the props.
this is controlling from parent. Example of controlled components. You can find about it more here: https://reactjs.org/docs/forms.html#controlled-components

Unable to implement Materialize-CSS initialization scripts with NextJS

I'm using Materialize CSS to style a web app I'm working on. Am building the app using NextJS with ReactJS. I have already styled the navbar using the classes defined in the Materialize CSS file. However, I'm unable to implement the responsive sidebar functionality using the prescribed initialization script as instructed on the Materialize website.
My Navbar component (./components/Navbar.js) looks like this:
import React, { Component, Fragment } from 'react';
import Link from 'next/link';
import $ from 'jquery';
class Navbar extends Component {
constructor(props) {
super(props)
this.props = props;
}
componentDidMount = () => {
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, options);
});
}
render() {
return (
<Fragment>
<nav>
<div className="nav-wrapper blue">
<Link prefetch href='/'>
Project Coco
</Link>
<i className="material-icons">menu</i>
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li>
<Link prefetch href='/'>
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href='/about'>
<a>About</a>
</Link>
</li>
<li>
</li>
</ul>
</div>
</nav>
{/* Sidenav markup */}
<ul className="sidenav" id="slide-out">
<li>
<Link prefetch href='/'>
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href='/about'>
<a>About</a>
</Link>
</li>
</ul>
</Fragment>
);
}
}
export default Navbar;
As is obvious, this functionality (sidebar) needs JQuery. So I already have it added via yarn and am invoking it using import $ from 'jquery'. But upon running, it throws an error saying sidenav is not a function.
I even tried entirely doing away with JQuery and just going for the vanilla JS version of the initialization script in componentDidMount:
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, options);
});
But this time, although no error, the functionality still refuses to work and clicking on the hamburger menu doesn't trigger the sidenav, which it should.
The entire code repo is up on Github for reference and the prototype app is live at schandillia.com. Any solutions?
P.S.: It seems the problem is that the initialization code in ComponentDidMount gets executed before MaterializeCSS.js (being called as an external file), on which it depends. Any work around this could be a potential solution, although that's just an assumption.
You need to import materialize-css into your component. Sadly, if you try to do import 'materialize-css' you'll get a window is undefined error. This is because Next.js is universal, which means it executes code first server-side where window doesn't exist. The workaround I used for this problem is the following:
Install materialize-css if you haven't:
$ npm i materialize-css
Import materialize-css into your component only if window is defined:
if (typeof window !== 'undefined') {
window.$ = $;
window.jQuery = $;
require('materialize-css');
}
Then, in your componentDidMount method:
componentDidMount = () => {
$('.sidenav').sidenav();
}
So your component looks like this:
import React, { Component, Fragment } from 'react';
import Link from 'next/link';
import $ from 'jquery';
if (typeof window !== 'undefined') {
window.$ = $;
window.jQuery = $;
require('materialize-css');
}
class Navbar extends Component {
constructor(props) {
super(props)
this.props = props;
}
componentDidMount = () => {
$('.sidenav').sidenav();
}
render() {
return (
<Fragment>
<nav>
...
</nav>
{/* Sidenav markup */}
<ul className="sidenav" id="slide-out">
...
</ul>
</Fragment>
);
}
}
export default Navbar;
Sources: Next.js FAQ and this issue.
How about this one:-
_app.tsx
import 'materialize-css/dist/css/materialize.min.css';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
index.tsx
import React, { useEffect } from 'react';
const Index = () => {
useEffect(() => {
const M = require('materialize-css');
M.toast({ html: 'worlds' });
}, []);
return (
<div className='container'>
<h1>hello</h1>
</div>
);
};
export default Index;

React Dynamic tag name

Assuming the following and all the components/fus/fci/ssg have just a single h1 with a site props. I want to understand why it is a valid react element yet these are not showing equally rendered. That is one has the h1 element and the other doesn't. The idea was to not create large component with toggles for different sites and each site would be swapped out based on the nav pick. I don't see anything documented for this unless I missed it...
{this.state.renderSite}
<Fci site="Fci"/>
import React from 'react';
import styles from './App.css';
import Nav from '../components/Nav.js'
import Fus from '../components/Fus.js'
import Fci from '../components/Fci.js'
import Ssg from '../components/Ssg.js'
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {renderSite: '', site: 'default' };
this.pickSite = this.pickSite.bind(this);
}
pickSite(site){
this.setState({renderSite: React.createElement(site, {"site":site})});
this.setState({site: site});
console.log( React.isValidElement(this.state.renderSite));
}
render() {
return (
<div className={styles.app}>
<Nav site={this.pickSite.bind(this)} />
{this.state.renderSite}
<Fci site="Fci"/>
</div>
);
}
}
The Nav
import React from 'react';
export default class Nav extends React.Component {
constructor(props) {
super(props);
this.update = this.update.bind(this);
}
update(e) {
this.props.site(e.target.dataset.site);
}
render(){
return (
<div>
<button onClick={this.update} data-site="Ssg"> SSG </button>
<button onClick={this.update} data-site="Fci"> FCI </button>
<button onClick={this.update} data-site="Fus"> FUS </button>
</div>
);
}
}
The problem is when you create the element you are passing a string (data-site value), not a component reference. So it ends up like this:
React.createElement("Fci");
As opposed to:
React.createElement(Fci);
Using a string will create a simple HTML element, not a component with with its own rendered content.
You could create a component map like this:
const componentMap = {
"Fci": Fci,
"Fus": Fus,
"Ssg": Ssg
}
Then from your string you can resolve a component reference:
React.createElement(componentMap[site], {site: site});
Or you could pass a component reference from your Nav:
<button onClick={this.update.bind(this, Ssg, "Ssg"}> SSG </button>
update(component, site, e) {
this.props.site(component, site);
}
pickSite(component, site) {
React.createElement(component, {site: site});
}