I just learned a little of react-redux and stuck at such problems I cannot understand and fix at least 4 days long.
First of the problem stands and can be seen at inspectors console (I use Chrome).
I have event handler at <div> inside react component. It have to be called at onClick event but it triggers at each load or reload of site.
Second, stands somewhere near reducer's function. It says me in console (dev tools) that reducers received action 'TOGGLE_TILE' and returned undefined instead of object. Should notice that reducer successfully receives state, action properties and makes some operations inside but as result nothing normal returnes.
The code of my reducer, actions, main, container, presentation components and functions provide. Please answer expanded as you can, i want to understand whats wrong and not make this mistake inside code twice.
ALSO! I using redux-thunk middleware (to functional callbacks inside actions, you know).
Inside i have:
index.js - main component
const store = createStore(reducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
actions.js
export function toggle(id){
return{
type: 'TOGGLE_TILE',
id
};
}
export function toggleTile(id){
return dispatch => {
console.log('toggling');
dispatch(toggle(id));
};
}
tiles.js - Reducer
var i = 0;
function tiles(state = tilesContainer, action){
var openedTiles = [];
switch (action.type) {
case 'TOGGLE_TILE':
if(i < 2){
console.log('i: '+i);
state.map((value) => {
var newOpen;
if(!value.opened && action.id === value.id){
newOpen = Object.assign({}, value, {
opened: !value.opened
});
openedTiles.push(newOpen);
i++;
console.log(i, value.opened, newOpen, openedTiles);
}
return newOpen, i;
});
}else if(i === 2){
var curr, prev;
openedTiles.map((value) => {
if(!prev){
prev = value;
}else{
curr = value;
console.log("Prev and curr: "+prev, curr);
if(curr.name === prev.name){
var currRes = Object.assign({}, curr, {
disappeared: !curr.disappeared
});
var prevRes = Object.assign({}, prev, {
disappeared: !prev.disappeared
});
return {currRes, prevRes};
} else {
let currRes = Object.assign({}, curr, {
opened: !curr.opened
});
let prevRes = Object.assign({}, prev, {
opened: !prev.opened
})
return currRes, prevRes;
}
}
});
}else{
return state;
}
default:
return state;
}
console.log("tiles: "+state.forEach(value => console.log(value)));
}
const reducers = combineReducers({
tiles
});
export default reducers;
AppContainer.jsx
const mapStateToProps = (state) => {
return {
tiles: state.tiles
};
};
const mapDispatchToProps = (dispatch) => {
return {
toggle: id => {
// console.log(id);
dispatch(toggleTile(id));
}
};
};
class AppContainer extends Component {
constructor(props){
super(props);
}
componentDidMount(){
}
render() {
var prop = this.props;
console.log(prop);
return (
<div>
<AppView prop={prop} />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);
AppView.js
class AppView extends React.Component {
constructor(props){
super(props);
this.state = {
tiles: this.props.prop.tiles,
};
this.showTiles = this.showTiles.bind(this);
this.defineRatio = this.defineRatio.bind(this);
this.toggleTile = this.toggleTile.bind(this);
}
componentDidMount(){
this.defineRatio();
}
componentWillMount(){
}
defineRatio(){
var imgClass;
let tile = document.querySelectorAll('img');
tile.forEach((value) => {
var imgSrc, imgW, imgH;
function defineImage(imgSrc){
var img = new Image();
img.src = imgSrc;
img.onload = function() {
return {
src:imgSrc,
width:this.width,
height:this.height};
};
return img;
}
var x = defineImage(value.src);
x.addEventListener('load',function(){
imgSrc = x.src;
imgW = x.width;
imgH = x.height;
// console.log(value.src, imgW, imgH);
var imgClass = (imgW / imgH > 1) ? 'wide' : 'tall';
value.classList += imgClass;
});
});
}
toggleTile(id){
this.props.prop.toggle(id);
}
showTiles(){
const boxElems = this.state.tiles.map((value, index) => {
var styles = {background: 'black'};
var tileState = value.opened ? '' : styles;
var imgState = value.opened ? 'opened ' : 'closed ';
var elem = <img key={value.id} src={value.src} alt="" className={imgState} />;
var boxElem = <div style={tileState} className="tile-box " onClick={this.toggleTile(value.id)} key={index}>{elem}</div>;
return boxElem;
});
return boxElems;
}
render(){
var tiles = this.showTiles();
return (
<div className="tiles-box">
<div className="tiles">
{tiles}
</div>
</div>
);
}
}
export default AppView;
First problem can be solved by replacing
onClick={this.toggleTile(value.id)}
with onClick={(e) => this.toggleTile(value.id)} First statement is just invoking this.toggleTile(value.id) immediately and setting the return value to OnClick event.
Regarding second you are not returning any thing from your reducer , hence state is undefined.
if(i < 2){
console.log('i: '+i);
state.map((value) => {
var newOpen;
if(!value.opened && action.id === value.id){
newOpen = Object.assign({}, value, {
opened: !value.opened
});
openedTiles.push(newOpen);
i++;
console.log(i, value.opened, newOpen, openedTiles);
}
return newOpen, i;
});
}
What is this return newOpen, i it should be return newOpen, also as this return is in a map function you have to return the mapped array as well
so use return state.map((value) => {
the problem that you have is that you are actually calling the function inside your div, thus it will get triggered each time you enter the view, so replace the following code on your showTiles()
var boxElem = <div style={tileState} className="tile-box " onClick={this.toggleTile(value.id)} key={index}>{elem}</div>;
to this:
var boxElem = <div style={tileState} className="tile-box " onClick={e => this.toggleTile(value.id)} key={index}>{elem}</div>;
and actually this should fix the error for the point 2.
Related
Context: I am rendering a 2-dimensional grid, where each cell is a Node object with properties: row, col, isGood. I create the grid and and initialize the initialGrid with a full grid.
Now I have onClick action that changes the property of isGood of clicked cell would change and it should render. However even if the change happens and the re-rendering does not happen.
Below is my code.
import { useState } from "react";
import "./App.css";
class Node {
constructor(x, y) {
this.row = x;
this.col = y;
this.isGood = false;
}
}
const totalRows = 5;
const totalCols = 5;
var grid = [];
const createNodes = () => {
grid = [];
for (let row = 0; row < totalRows; ++row) {
var newRow = [];
for (let col = 0; col < totalCols; ++col) {
newRow.push(new Node(row, col));
}
grid.push(newRow);
}
console.log(grid);
};
createNodes();
const App = () => {
const [initialGrid, setGrid] = useState(grid);
const handleClick = (rowIndex, colIndex) => {
initialGrid[rowIndex][colIndex].isGood = true;
setGrid(initialGrid);
};
const getNodeClass = (node) => {
const newClassName = node.isGood === true ? "node-good" : "node";
return newClassName;
};
return (
<div>
{initialGrid.map((row, rowIndex) => {
return (
<div key={rowIndex} className="rows">
{row.map((column, columnIndex) => {
return (
<div
className={getNodeClass(column)}
id={`${rowIndex}-${columnIndex}`}
key={columnIndex}
onClick={() => {
handleClick(rowIndex, columnIndex);
}}
></div>
);
})}
</div>
);
})}
</div>
);
};
export default App;
How can I re-render the grid ?
my onClick works and I can see it on console.log however the re-rendering doesnot happen even if I call setGrid
You need to construct a new array in the call to setGrid or React won't realize it needs to re-render. You can do this with the spread operator, adding the individual elements of the old array to your new array:
const handleClick = (rowIndex, colIndex) => {
initialGrid[rowIndex][colIndex].isGood = true;
setGrid([...initialGrid]);
};
We're also told React works better if we make the call to setGrid into a function as below, which also works. Personally I'm not sure if this is strictly necessary in this example and the code is arguably less clear:
const handleClick = (rowIndex, colIndex) => {
initialGrid[rowIndex][colIndex].isGood = true;
setGrid((ary) => [...ary]);
};
I am trying to add a button tag between a text when a pattern is found in react, this is done inside the dangerouslySetInnerHTML attribute. The onClick only works with alert() and console.log(), it doesn't work when I passed a function to it. What am I missing?
export const detectHashTagPattern = (text) => {
if(!text) return '';
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
let hashTagPattern = /#[0-9]+/;
text = text.replace(pattern, (res) => {
return res.replace(hashTagPattern, `<button onClick={}>${hashTagPattern.exec(res)}</button>`);
});
return text;
};
Well you could change the algorithm like the following:
function clickHandler() {
console.log("do something");
}
window.clickHandler = clickHandler;
export const detectHashTagPattern = (text) => {
if (!text) return "";
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
let hashTagPattern = /#[0-9]+/;
text = text.replace(pattern, (res) => {
return res.replace(
hashTagPattern,
// use window.<fct>()
`<button onClick="window.${window.clickHandler.name}()">${hashTagPattern.exec(
res
)}</button>`
);
});
return text;
};
You shouldn't go with this approach, as it might have other issues with the rendering in your application (depends what you're doing in the handler).
A better approach would be like this:
const splitSubject = (text) => {
if (!text) {
return [""];
}
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
if (!pattern.test(text)) {
return [text];
}
let hashTagPattern = /#[0-9]+/;
let result = text.search(hashTagPattern);
return [text.slice(0, result), text.slice(result), text.slice(result + 1)];
};
const Subject = ({ subject, handler }) => {
const splitted = splitSubject(subject);
let content = null;
if (splitted.length === 1) {
content = <span>{splitted[0]}</span>;
} else {
let [info, cmd, reqReference] = splitted;
content = (
<>
<span>{info}</span>
<button onClick={() => handler?.(reqReference)}>{cmd}</button>
</>
);
}
return <p>{content}</p>;
};
export default function App() {
const requestHandler = (num) => {
console.log(`click on '${num}'`);
};
return (
<div>
<Subject
handler={requestHandler}
subject="This is a follow-up to your previous request #9"
/>
<Subject handler={requestHandler} subject="Not matching subject" />
</div>
);
}
I'm trying to have my Carousel height resize dynamically upon change. However I can't seem trigger a state change purely from a childs height change.
Listening to children was pretty good, infact I'm not fully sure why it's not working.
The problem occurs when an error message is appended to a child within the carousel. It doesn't update.
Currently the best thing I know of to do is an interval...
Is there a better way?
import React, {useState, useEffect, useRef} from 'react';
import './Carousel.scss';
// Wrapped children components must be placed inside of <div> elements
const Carousel = ({slideTo, children}) => {
const [carouselStyle, setCarouselStyle] = useState({});
const activeRef = useRef(null);
const index = slideTo - 1
const newChildren = [];
children.map((d,i) => {
let addClass = (d.props.className !== undefined) ? d.props.className: ""
const newProps = {
key: i
};
if(i === index){
addClass += " active"
newProps.ref = activeRef;
}
newProps.className = addClass.trim();
const newChild = React.cloneElement(d, newProps);
newChildren.push(newChild);
return false
});
const carouselContainerStyle = {
left: (((slideTo * 100) - 100) * -1) + "%",
width: (newChildren.length * 100)+ "%"
}
useEffect(() => {
const interval = setInterval(function(){
console.log("int");
if(activeRef != null){
if(carouselStyle.height === undefined || carouselStyle.height !== activeRef.current.clientHeight){
setCarouselStyle({
height: activeRef.current.clientHeight,
});
}
}
},50)
return () => {
clearInterval(interval)
}
},[]);
useEffect(() => {
console.log("children update");
if(activeRef.current !== null){
setCarouselStyle({
height: activeRef.current.clientHeight,
});
}
},[slideTo,children]);
return (
<div className="carousel" style={carouselStyle}>
<div style={carouselContainerStyle} className="carousel-container">
{newChildren}
</div>
</div>
);
};
export default Carousel;
Implementation
<Carousel slideTo={slide}>
<div><SignIn/></div>
<div><SignUp/></div>
</Carousel>
When I try to refresh the usestate my program just breaks. The problematic line is "setMesses(_messages);". I tried to capitalized the usestate but nothing has changed.
import React, {useState} from 'react';
import Message from './Message';
import * as firebase from "firebase";
function MessContainer() {
let counter = 0;
let _messages = [];
const [messes, setMesses] = useState([{this: null}]);
firebase.database().ref().child('counter').on('value', function(snapshot){
counter = snapshot.child("counter").val();
});
function load(_counter){
firebase.database().ref().child('messages/' + _counter).on('value', function(snapshot){
let _chet = {};
let _name = snapshot.child("name").val();
_chet.mess = _name + ": " + snapshot.child("message").val();
if(_name === document.getElementById("name").value){
_chet.status = "right";
} else {
_chet.status = "left";
}
_messages.push(_chet);
});
}
function loadChet(){
_messages = [];
for(let i = 0; i < counter; i++){
load(i);
}
console.log(_messages);
setMesses(_messages);
setTimeout(loadChet, 1000);
}
loadChet();
return (
<div>{messes.map(_mess => (
<Message mess={_mess.mess} status={_mess.status} />
))}
</div>
);
}
export default MessContainer;
The reason why this happens is because you call loadChet and this calls setMesses wich makes the component rerender and call loadChet again, causing a infinity loop.
You shouldn't call loadChet on the function, maybe use useEffect and call it only once will. When do you need to call loadChet ?
Edit:
Try this
function MessContainer() {
let counter = 0;
let _messages = [];
const [messes, setMesses] = useState([{this: null}]);
firebase.database().ref().child('counter').on('value', function(snapshot){
counter = snapshot.child("counter").val();
});
function load(_counter){
firebase.database().ref().child('messages/' + _counter).on('value', function(snapshot){
let _chet = {};
let _name = snapshot.child("name").val();
_chet.mess = _name + ": " + snapshot.child("message").val();
if(_name === document.getElementById("name").value){
_chet.status = "right";
} else {
_chet.status = "left";
}
_messages.push(_chet);
});
}
function loadChet(){
_messages = [];
for(let i = 0; i < counter; i++){
load(i);
}
console.log(_messages);
setMesses(_messages);
setTimeout(loadChet, 1000);
}
useEffect(() => {
loadChet();
}, [])
return (
<div>{messes.map(_mess => (
<Message mess={_mess.mess} status={_mess.status} />
))}
</div>
);
}
I am trying to set the state in React inside my function, however I get an error message stating: Cannot read property 'setState' of undefined.
Below is my code, I call the state in the constructor then I am setting the state in the addTimes function and binding this to that function, however I am still getting the error.
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
times: []
};
}
render(){
Array.prototype.remove = function() {
var what, a = arguments, L = a.length, ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
};
var currentTicked = [];
var times =[]
function addTimes(id){
var index = times.indexOf(id);
if (!times.includes(id)) {
$("input:checkbox[name=time]:checked").each(function(){
currentTicked.push($(this).val());
times = times.concat(currentTicked)
times = jQuery.unique(times);
});
} else if(times.includes(id)){
times.remove(id)
}
console.log(times);
addTimes = () => {
this.setState({
times: times
});
}
}
you haven't bound the function to the class. try doing
addTimes = (id) => {
// code here
}
in the component class
Try with an arrow function:
addTimes = id => {
var index = times.indexOf(id);
if (!times.includes(id)) {
$("input:checkbox[name=time]:checked").each(function(){
currentTicked.push($(this).val());
times = times.concat(currentTicked)
times = jQuery.unique(times);
});
} else if(times.includes(id)){
times.remove(id)
}
console.log(times);
addTimes = () => {
this.setState({
times: times
});
}
}
Or, bind thisin addTimesfunction like this :
constructor(props) {
super(props);
this.state = {
times: []
this.addTimes = this.addTimes.bind(this);
};
}
better to use es6 syntax. try with below code.
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
times: []
};
let times = [];
let currentTicked = [];
this.addTimes = this.addTimes.bind(this);
}
addTimes(id) {
let index = times.indexOf(id);
if (!times.includes(id)) {
$("input:checkbox[name=time]:checked").each(function(){
currentTicked.push($(this).val());
times = times.concat(currentTicked)
times = jQuery.unique(times);
});
} else if(times.includes(id)){
times.remove(id)
}
this.setState({
times: times
});
}
render(){
this.addTimes;
return (
Array.prototype.remove = function() {
var what, a = arguments, L = a.length, ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
}
);
}
}