React Sortable JS with mobx - reactjs

I am building a functional component that should render a sortable component (ReactSortable) with computed array (layers array) that comes from a mobx store using react-sortablejs. The component renders when the apps load, but when resorting the array by dragging and dropping elements, I get the following error:
mobx.module.js:89 Uncaught Error: [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an action if this change is intended. Tried to modify: LayerStore#23.layersRegistry.9f332395-6bfe-4c3f-a496-8804e1dec490.chosen?
Actually, the layers array is computed from layersRegistry Map observable in the store as the code below shows, but when sorting the layers, I am not changing the layersRegistry observable; that’s why I got confused from the error.
Whats the problem in this case, and how can I resolve it such that when I resort the layers (by drag drop) the layers variable in the store or the layersRegistry Map observable is updated based on the sorting?
Here the code for the component
import React, { useContext, useEffect, Fragment, useState } from "react";
import { RootStoreContext } from "../../../app/stores/rootStore";
import { observer } from "mobx-react-lite";
import LayerListItem from "./LayerListItem";
import { ReactSortable } from "react-sortablejs";
export const LayersDashboard = () => {
const rootStore = useContext(RootStoreContext);
const { layers, loadLayers } = rootStore.layerstore;
useEffect(() => {
loadLayers();
}, [loadLayers]);
const [items, setItems] = useState(layers);
return (
<Fragment>
<ReactSortable list={layers} setList={setItems}>
{layers.map((layer) => (
<LayerListItem key={layer.id} layer={layer} />
))}
</ReactSortable>
</Fragment>
);
};
export default observer(LayersDashboard);
Here is the code for the store
import { observable, action, runInAction, computed } from "mobx";
import agent from "../api/agent";
import { RootStore } from "./rootStore";
import { ILayer } from "../models/Layer";
export default class LayerStore {
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
#observable layersRegistry = new Map();
#computed get layers() {
return Array.from(this.layersRegistry.values());
}
#action loadLayers = async () => {
try {
const layers = await agent.Layers.list();
runInAction(() => {
layers.forEach((layer) => {
this.layersRegistry.set(layer.id, layer);
});
});
} catch (error) {
runInAction(() => {
console.log("error");
});
}
};
}

I would suggest to call toJS on your data before passing it to ReactSortable or any other library and to keep your observable data in sync with sortable data you will need to use some onDragEnd event hook and call mobx action from there to update observable data. But I'm not sure if react-sortablejs supports this event hook.

Related

Invalid hook call error when showing/hiding component with React Redux

I'm attempting to create a React/Redux component that shows/hides an element when clicked.
I'm using this to trigger the function from another component:
import React from 'react'
//Some other code...
import { useSelector } from 'react-redux'
import onShowHelpClicked from '../help/AddHelpSelector'
<button onClick={onShowHelpClicked}>Help</button>
This this is AddHelpSelector:
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import { helpVisible } from './HelpSlice'
export const AddHelp = () => {
const [isVisible, showHelp] = useState('')
const dispatch = useDispatch()
const onShowHelpClicked = () => {
dispatch(
helpVisible({
isVisible,
})
)
if (isVisible) {
showHelp(false)
} else {
showHelp(true)
}
}
return (
<section>
<h2 style={{ visibility: { isVisible } }}>Help section</h2>
</section>
)
}
export default AddHelp
Finally, this is HelpSlice
import { createSlice } from '#reduxjs/toolkit'
const initialState = [{ isVisible: false }]
const helpSlice = createSlice({
name: 'help',
initialState,
reducers: {
helpVisible(state, action) {
state.push(action.payload)
},
},
})
export const { helpVisible } = helpSlice.actions
export default helpSlice.reducer
I'm fairly certain I'm doing multiple things wrong, as this is my first attempt to do anything with Redux and I'm still struggling to wrap my mind around it after a week of learning.
But specifically, when clicking the help button I get this error.
"Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app"
The linked documentation provides a way to test a component to see if React is importing properly, and it's not. But I'm not really sure what I'm doing wrong.
I think it may be that I'm importing React multiple times, but if I don't then I can't use "useState."
What's the correct way to do this? I'm open to corrections on both my code as well as naming conventions. I'm using boilerplate code as a template as I try to understand this better after getting through the documentation as well as Mosh's 6 hour course which I just finished.
You're importing the < AddHelpSelector /> component here import onShowHelpClicked from '../help/AddHelpSelector', and then you try to use it as a callback handler for the button's onClick, which doesn't really make sense. I assume you actually wanted to only import the onShowHelpClicked function declared inside the < AddHelpSelector /> component (which is not really a valid way of doing it). Since you want to control the visibility using redux state, you could just grab the flag from the redux store inside the < AddHelpSelector /> component using useSelector hook. To set it, you're gonna do that in the component where your button is. For that, you just need to dispatch an action(like you already did), with the updated flag. No need for the local useState. Also, using the flag you could just conditionally render the element.
const App = () => {
const dispatch = useDispatch();
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
const handleClick = () => {
dispatch(
helpVisible({
!isVisible,
})
)
}
return (<button onClick={handleClick}>Help</button>);
}
export const AddHelp = () => {
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
return (
<section>
{isVisible && <h2>Help section</h2>}
</section>
)
}
export default AddHelp

propblem to call action function is redux i have a message : this.props.nameofprops.action() is not a fction

I'm writing a react-redux code I defined an action to called in the componant, it's called addCart.
import axios from "axios"
import {GET_PLATS} from "./actionType"
export const getplats = () => dispatch => {
// 2-1 axions get the same path of back in app.use
axios.get("/plat-list").then(res => {
dispatch({
//2-2 same name in action type (after this go to make reducers)
type:GET_PLATS,
payload:res.data
})
})
}
export const addCart =( ) =>{
return (dispatch) => {
console.log("added To cart");
dispatch({
type:GET_PLATS,
})
}
}
then I've wrote this reducer:
import { GET_PLATS } from "../action/actionType"
//first create first main state
const initialState={
plats:[],
cmdElements:[]
}
export default function(state=initialState,action){
switch(action.type){
case GET_PLATS:
return{
...state,
plats:action.payload,
cmdElements:state.cmdElements
}
default :
return state
}
}
Then I called this action in a component**
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {getplats,addCart } from '../../action/action'
import PropTypes from 'prop-types'
const { Meta } = Card
class PlatListeU extends Component {
componentDidMount(){
this.props.getplats()
}
render() {
return (
<div>
<Button type="primary" onClick={ () =>{
this.props.platListe.addCart()
} } >addTocart</Button>
</div>
)}
PlatListeU.propTypes = {
addCart:PropTypes.func.isRequired,
PlatListe:PropTypes.object.isRequired
}
const mapStateToProps =(state) =>{
return{
platListe:state.plats ,
cmdElements:state.cmdElements
}
}
export default connect(mapStateToProps,{ getplats,addCart}) (PlatListeU)
But when I press on the button, I have this error message:
TypeError: this.props.platListe.addCart is not a function
Everything should be fine. I tried a lot of ways but the result is the same . Can anyone help me?
It's just this.props.addCart(). You import the methods in the component, not in platListe. Thus the error.
For the other error:
it looks like your reducers are mapped correctly. so state.moviesReducer is always defined.
if your initial list of items doesn't even load correctly, means your items object are causing the error.
if your initial list loads correctly, and the error occurs only after you dispatch the action, means your update() is mutating your state shape.
I don't know where you are getting this update function. but i am guessing you should just return{items:update(..)}
This is how I would go about debugging your code.
I think it is just this.props.addCart()
I believe you just want this.props.addCart(). The key platListe exists in your state object, it does not have any methods attached to it. You import the standalone methods at the top of your component.

React hook "useSelector" is called in function error for my permissions context provider

I'm trying to make a permissions provider that wraps some react-redux global state. I have as follows:
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
export const PermissionsContext = React.createContext();
const getUserAbilities = createSelector(
state => state.user.abilities,
abilities =>
abilities.reduce((acc, val) => {
acc[val] = true;
return acc;
}, {})
);
function useAbilities() {
const abilities = useSelector(getUserAbilities);
return { abilities };
}
export const PermissionsProvider = ({ children }) => {
const { abilities } = useAbilities();
const can = useCallback((...permissions) => permissions.every(permission => permission in abilities), [
abilities
]);
return <PermissionsContext.Provider value={{ can }}>{children}</PermissionsContext.Provider>;
};
export const withPermissions = WrappedComponent => {
return class ComponentWithPermissions extends React.Component {
render() {
return (
<PermissionsContext.Consumer>
{props => <WrappedComponent {...this.props} permissions={props} />}
</PermissionsContext.Consumer>
);
}
};
};
Usage of PermissionsProvider:
<PermissionsProvider>
<App />
</PermissionsProvider>
This includes a context so I can useContext(PermissionsContext) and also a HOC withPermissions so that I wrap legacy class components with it.
In the case of a class, I would call this.props.permissions.can('doThing1', 'doThing2') and it should return true or false depending on whether all of those abilities are present in the user payload.
It seems to be functioning fine except when I try to commit it, I get the error:
React Hook "useSelector" is called in function "can" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I saw a few issues with naming convention, but that doesn't seem to apply here(?). I also used to have the useAbilities hook inside the function just above the can function which also threw the error.
Any ideas?
All the above code was correct. It seemed to be some sort of eslint cache that was continuing to complain. After clearing cache and rerunning lint it went away.

transitioning to use redux with hooks

figuring out how to use redux with hooks using this way but not sure its the correct way
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPins } from "../../../actions/pins";
function MainStory() {
const pins = useSelector(state => state.pins.pins);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPins(pins));
}, []);
console.log(pins);
return(<div> test </div>
would the above way be the right way to go despite missing dependencies?
React Hook useEffect has missing dependencies: 'dispatch' and 'pins'. Either include them or remove the dependency array
with components (the way i had it before)
import { getPins } from "../../actions/pins";
import { connect } from "react-redux";
import PropTypes from "prop-types";
export class Pins extends Component {
static propTypes = {
pins: PropTypes.array.isRequired,
getPins: PropTypes.func.isRequired
};
componentDidMount() {
this.props.getPins();
}
render() {
console.log(this.props.pins);
return <div>test</div>;
}
}
const mapStateToProps = state => ({
pins: state.pins.pins
});
export default connect(mapStateToProps, { getPins })(Pins);
the plan is to list each pin
You can add dispatch to the dependencies list, since it won't change. If you'll add the pins to dependancies list of the useEffect block, you might cause infinite loop, if the response to the action changes the state (call to server that returns an array).
However, according to the class component example, the getPins() action creator doesn't require the value of pins, so you can just add dispatch to the list of dependancies:
function MainStory() {
const pins = useSelector(state => state.pins.pins);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPins());
}, [dispatch]);
console.log(pins);
return(<div> test </div>

Can I replace context with hooks?

Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}

Resources