I will preface this with stating this is my fourth day working on Node or React.js, so please bear with me.
I am building a custom, offline search function for Docusaurus 2. I have built a JSON index and created a function to search it with elasticlunr. I want to redirect to a separate results page, however I am having issues with the redirect despite trying to follow multiple examples. Here is my index.js for the SearchBar.
import React, {Component} from 'react';
import {Redirect} from 'react-router-dom';
import classnames from 'classnames';
import elasticlunr from 'elasticlunr';
let siteIndex = require('./siteIndex.json');
class Search extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
term: '',
index: elasticlunr(function () {
this.setRef('id');
this.addField('title');
this.addField('body');
this.addField('url');
})
};
this.toggleSearchPressEnter = this.toggleSearchPressEnter.bind(this);
this.changeTerm = this.changeTerm.bind(this);
}
init() {
let siteIndexKeys = Object.keys(siteIndex);
siteIndexKeys.forEach(key => {
this.state.index.addDoc(siteIndex[key]);
});
}
changeTerm(e) {
this.setState({term: e.target.value});
}
toggleSearchPressEnter(e) {
if (e.key === "Enter") {
this.init();
let siteSearch = this.state.index.search(e.target.value, {}); // Empty dictionary here fixes the warning about using the default configuration because you didn't supply one!
let docs = this.state.index.documentStore.docs;
this.state.results = siteSearch.slice(0, 5).map(searchKey => docs[searchKey.ref]);
if (this.state.results.length > 0) {
this.renderRedirect();
}
}
}
renderRedirect() {
console.log("Go home!");
console.log(this.state.results.length);
console.log(this.state.results);
// window.location = "/"
<Redirect
to={{
pathname: '/',
state: { results: this.state.results }
}}
/>
}
render() {
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames('search-icon', {
'search-icon-hidden': this.props.isSearchBarExpanded,
})}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
'navbar__search-input',
{'search-bar-expanded': this.props.isSearchBarExpanded},
{'search-bar': !this.props.isSearchBarExpanded},
)}
onKeyPress={this.toggleSearchPressEnter}
/>
</div>
);
}
}
export default Search;
Because we had issues redirecting to the results page with the results, I wanted to see if I could just go to the home page. I see the message "Go home!" in the browser console when the user hits enter on the search bar, but no redirect occurs. I have commented out the javascript redirect that does work if I comment out Redirect from renderRedirect().
I have tried adding a return() around the Redirect, but it does not seem to make any difference.
If you would like to reproduce the issue
npx #docusaurus/init#next init docs classic
npm run swizzle #docusaurus/theme-search-algolia SearchBar
Replace the contents of src/theme/SearchBar/index.js with the code that is the problem above.
To generate the JSON index:
generate-index.js
const fs = require('fs-extra');
const path = require('path');
const removeMd = require('remove-markdown');
let searchId = 0;
const searchDoc = {};
async function readAllFilesAndFolders(folder) {
try {
const topFilesAndFolders = fs.readdirSync(folder);
for (let i = 0; i < topFilesAndFolders.length; i++) {
const file = topFilesAndFolders[i];
const fileOrFolderPath = `${folder}/${file}`;
const stat = fs.lstatSync(fileOrFolderPath);
if (stat.isFile() && path.extname(fileOrFolderPath) === '.md') {
console.log(`Got Markdown File ${file}`);
fs.readFile(fileOrFolderPath, (err, data) => {
if (err) throw err;
const regex = /title: .*\n/g;
let search = data.toString().match(regex);
let docTitle = search[0].toString().replace("title: ", "");
console.log("doctitle: ", docTitle);
if (!docTitle) {
docTitle = file.replace('.md', '');
generateSearchIndexes(fileOrFolderPath, file, docTitle);
}
else {
generateSearchIndexes(fileOrFolderPath, file, docTitle);
}
});
} else if (stat.isDirectory()) {
console.log(`Got Directory ${file}, Started Looking into it`);
readAllFilesAndFolders(fileOrFolderPath, file);
}
}
} catch (error) {
console.log(error);
}
}
function generateSearchIndexes(fileOrFolderPath, file, docTitle) {
try {
let fileContent = fs.readFileSync(fileOrFolderPath, 'utf-8');
let body = removeMd(fileContent).replace(/^\s*$(?:\r\n?|\n)/gm, '');
let title = docTitle.trim();
let url = fileOrFolderPath
.replace('.md', '')
.trim();
searchDoc[file.replace('.md', '').toLowerCase()] = { id: searchId, title, body, url };
fs.writeFileSync('src/theme/SearchBar/siteIndex.json', JSON.stringify(searchDoc), 'utf-8');
searchId = searchId + 1;
} catch (error) {
console.log('Failed to generate fail:', error);
}
}
readAllFilesAndFolders('docs');
Once the JSON index is built from the default docs, the search can be attempted. I haven't made any other changes.
I've probably done something stupid and hopefully it is easily fixable, so please be merciful. I really did try. ;)
Using some guidance from Ajay, and playing around a little, I have a working solution.
import React, {Component} from 'react';
import {Redirect} from 'react-router';
import classnames from 'classnames';
import elasticlunr from 'elasticlunr';
let siteIndex = require('./siteIndex.json');
class Search extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
term: '',
search: '',
index: elasticlunr(function () {
this.setRef('id');
this.addField('title');
this.addField('body');
this.addField('url');
})
};
this.toggleSearchPressEnter = this.toggleSearchPressEnter.bind(this);
this.changeTerm = this.changeTerm.bind(this);
}
init() {
let siteIndexKeys = Object.keys(siteIndex);
siteIndexKeys.forEach(key => {
this.state.index.addDoc(siteIndex[key]);
});
}
changeTerm(e) {
this.setState({term: e.target.value});
}
toggleSearchPressEnter(e) {
if (e.key === "Enter") {
this.init();
let searchTerm = e.target.value;
let siteSearch = this.state.index.search(searchTerm, {}); // Empty dictionary here fixes the warning about using the default configuration because you didn't supply one!
let docs = this.state.index.documentStore.docs;
let searchResults = siteSearch.slice(0, 5).map(searchKey => docs[searchKey.ref]);
this.setState({
results: searchResults,
search: searchTerm,
});
}
}
render() {
if (this.state.results.length >= 1) {
return <Redirect to={{
pathname: '/results',
state: {
results: this.state.results,
search: this.state.search
}
}} />
}
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames('search-icon', {
'search-icon-hidden': this.props.isSearchBarExpanded,
})}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
'navbar__search-input',
{'search-bar-expanded': this.props.isSearchBarExpanded},
{'search-bar': !this.props.isSearchBarExpanded},
)}
onKeyPress={this.toggleSearchPressEnter}
/>
</div>
);
}
}
export default Search;
Related
I have these two components: I fetch the data in the parent component and then pass it to the child component, which formats it into an object so that it can be used in the construction of a graph.
When the child component is mounted for the first time, the graph is not rendered.
If I try to change the child component code to update it (e.g. delete linkColor from ForceGraph2D’s props), the graph will be displayed correctly.
I want to find a way to see it right away, the first time the component is mounted.
Parent component:
import React, {Fragment, useEffect, useState} from "react";
import AddressList from "./AddressList";
import ClusterGraph from "./ClusterGraph";
import axios from "axios";
function ClusterPage(props) {
const [cluster, setCluster] = useState([]);
const [subCluster, setSubCluster] = useState([]);
useEffect(() => {
fetchData1();
fetchData2();
},[]);
const fetchData1 = async () => {
const address = props.match.params.addressHash;
const response = await axios.get('http://localhost:5000/', {
params: {address: address}
});
const data = await response.data;
setCluster(data);
}
const fetchData2 = async () => {
const address = props.match.params.addressHash;
const response = await axios.get('http://localhost:5000/sub/', {
params: {address: address}
});
const data = await response.data;
setSubCluster(data);
}
return (
<div key={props.match.params.id}>
<Fragment>
<AddressList data={cluster} />
<ClusterGraph data1={cluster} data2={subCluster} />
</Fragment>
</div>
);
}
export default ClusterPage;
Child component:
import '../styles/ClusterGraph.css';
import ForceGraph2D from 'react-force-graph-2d';
import React from "react";
class MyClusterGraph extends React.Component {
constructor(props) {
super(props);
this.state = {nodes:[],links:[]};
}
componentWillMount() {
this.loadData();
}
loadData = async () => {
let nodes = this.props.data1.map(row => {
let id = row.address_id;
let addressHash = row.address_hash;
let nodeColor;
if(row.miner_address)
nodeColor="blue";
else
nodeColor="purple";
return {id:id,addressHash:addressHash,nodeColor:nodeColor};
});
let links = this.props.data2.map(row => {
let source = row.address_id_1;
let target = row.address_id_2;
let linkColor;
switch (row.link_type) {
case 0:
linkColor="green";
break;
case 1:
linkColor="red";
break;
case 2:
linkColor="cyan";
break;
}
return {source:source,target:target,linkColor:linkColor};
});
this.setState({nodes:nodes,links:links});
}
render() {
return (
<div className="graph">
<ForceGraph2D
graphData={this.state}
backgroundColor="white"
height={400}
width={700}
nodeLabel="addressHash"
nodeColor="nodeColor"
linkColor="linkColor" />
</div>
);
}
}
function ClusterGraph({data1,data2}) {
return (
<div className="section2">
<MyClusterGraph data1={data1} data2={data2} />
</div>
);
}
export default ClusterGraph;
You could make sure you render your graphs after the data is fully fetched, and show a loader in the meantime, like so:
function ClusterPage(props) {
const [cluster, setCluster] = useState([]);
const [subCluster, setSubCluster] = useState([]);
useEffect(() => {
fetchData1();
fetchData2();
}, []);
const fetchData1 = async () => {
const address = props.match.params.addressHash;
const response = await axios.get("http://localhost:5000/", {
params: { address: address },
});
const data = await response.data;
setCluster(data);
};
const fetchData2 = async () => {
const address = props.match.params.addressHash;
const response = await axios.get("http://localhost:5000/sub/", {
params: { address: address },
});
const data = await response.data;
setSubCluster(data);
};
if (cluster.length <= 0 || subCluster.length <= 0) {
return <p>Loading...</p>;
}
return (
<div key={props.match.params.id}>
<Fragment>
<AddressList data={cluster} />
<ClusterGraph data1={cluster} data2={subCluster} />
</Fragment>
</div>
);
}
This way, you would use the constructor to format your data, as componentWillMount() is deprecated and considered unsafe:
class MyClusterGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: this.props.data1.map((row) => {
let id = row.address_id;
let addressHash = row.address_hash;
let nodeColor;
if (row.miner_address) nodeColor = "blue";
else nodeColor = "purple";
return { id: id, addressHash: addressHash, nodeColor: nodeColor };
}),
links: this.props.data2.map((row) => {
let source = row.address_id_1;
let target = row.address_id_2;
let linkColor;
switch (row.link_type) {
case 0:
linkColor = "green";
break;
case 1:
linkColor = "red";
break;
case 2:
linkColor = "cyan";
break;
}
return { source: source, target: target, linkColor: linkColor };
}),
};
}
render() {
return (
<div className="graph">
<ForceGraph2D
graphData={this.state}
backgroundColor="white"
height={400}
width={700}
nodeLabel="addressHash"
nodeColor="nodeColor"
linkColor="linkColor"
/>
</div>
);
}
}
function ClusterGraph({ data1, data2 }) {
return (
<div className="section2">
<MyClusterGraph data1={data1} data2={data2} />
</div>
);
}
export default ClusterGraph;
Try making your parent component calls synchronous currently it is sync individually but it calls in async fashion change your code base accordingly
I am trying to use React Scheduler with my shifts database. The current state after trying to use hooks instead of class is that I cannot edit any field in the form. I have deleted some of the code to make it cleaner, for now I am trying only to add a shift.
React Scheduler original code:
import * as React from 'react';
import Paper from '#material-ui/core/Paper';
import { ViewState, EditingState } from '#devexpress/dx-react-scheduler';
import {
Scheduler,
Appointments,
AppointmentForm,
AppointmentTooltip,
WeekView,
} from '#devexpress/dx-react-scheduler-material-ui';
import { appointments } from '../../../demo-data/appointments';
export default class Demo extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
data: appointments,
currentDate: '2018-06-27',
addedAppointment: {},
appointmentChanges: {},
editingAppointment: undefined,
};
this.commitChanges = this.commitChanges.bind(this);
this.changeAddedAppointment = this.changeAddedAppointment.bind(this);
this.changeAppointmentChanges = this.changeAppointmentChanges.bind(this);
this.changeEditingAppointment = this.changeEditingAppointment.bind(this);
}
changeAddedAppointment(addedAppointment) {
this.setState({ addedAppointment });
}
changeAppointmentChanges(appointmentChanges) {
this.setState({ appointmentChanges });
}
changeEditingAppointment(editingAppointment) {
this.setState({ editingAppointment });
}
commitChanges({ added, changed, deleted }) {
this.setState((state) => {
let { data } = state;
if (added) {
const startingAddedId = data.length > 0 ? data[data.length - 1].id + 1 : 0;
data = [...data, { id: startingAddedId, ...added }];
}
return { data };
});
}
render() {
const {
currentDate, data, addedAppointment, appointmentChanges, editingAppointment,
} = this.state;
return (
<Paper>
<Scheduler
data={data}
height={660}
>
<ViewState
currentDate={currentDate}
/>
<EditingState
onCommitChanges={this.commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={this.changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={this.changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={this.changeEditingAppointment}
/>
<WeekView
startDayHour={9}
endDayHour={17}
/>
<Appointments />
<AppointmentTooltip
showOpenButton
showDeleteButton
/>
<AppointmentForm />
</Scheduler>
</Paper>
);
}
}
My function component code:
import React, { useState } from 'react';
import Paper from '#material-ui/core/Paper';
import { ViewState, EditingState } from '#devexpress/dx-react-scheduler';
import {
Scheduler,
Appointments,
AppointmentForm,
AppointmentTooltip,
WeekView,
ConfirmationDialog,
} from '#devexpress/dx-react-scheduler-material-ui';
const DataSheet = ( { addShift, shifts, deleteShift } ) => {
const [data, setData] = useState(shifts)
const [currentDate, setCurrentDate] = useState('2018-06-27')
const [addedAppointment, setAddedAppointment] = useState({})
const [appointmentChanges, setAppointmentChanges] = useState({})
const [editingAppointment, setEditingAppointment] = useState(undefined)
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment({ addedAppointment });
}
const changeAppointmentChanges = (appointmentChanges) => {
setAppointmentChanges({ appointmentChanges });
}
const changeEditingAppointment = (editingAppointment) => {
setEditingAppointment({ editingAppointment });
}
const commitChanges = ({ added, changed, deleted }) => {
setData ((????) => {
let { data } = data;
console.log(data); //returns undefined
if (added) {
const startingAddedId = data > 0 ? data[data.length - 1].id + 1 : 0;
data = [...data, { id: startingAddedId, ...added }];
addShift(added);
}
return { data };
});
}
return (
<Paper>
<Scheduler
data={data}
height={660}
>
<ViewState
currentDate={currentDate}
/>
<EditingState
onCommitChanges={commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={changeEditingAppointment}
/>
<WeekView
startDayHour={9}
endDayHour={17}
/>
<Appointments />
<AppointmentTooltip
showOpenButton
showDeleteButton
/>
<AppointmentForm />
</Scheduler>
</Paper>
);
}
export default DataSheet
App.js:
import React from 'react';
import backgroundImage from './Resources/BennyBackground.jpeg'
import Header from "./components/Header";
import { useState, useEffect } from "react"
import DataSheet from './components/DataSheet';
const containerStyle= {
width: '100vw',
height: '100vh',
backgroundImage: `url(${backgroundImage})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
}
const App = () => {
const [shifts, setShifts] = useState([])
useEffect(() => {
const getShifts = async () => {
const shiftsFromServer = await fetchShifts()
setShifts(shiftsFromServer)
}
getShifts()
}, [])
const fetchShifts = async () => {
const res = await fetch(`http://localhost:5000/shifts/`)
const data = await res.json()
return data
}
const addShift = async (shift) => {
const startingAddedId = shifts.length > 0 ? shifts[shifts.length - 1].id + 1 : 0;
shift.id = startingAddedId;
const res = await fetch(`http://localhost:5000/shifts/`,{
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(shift)
})
const data = await res.json()
setShifts([...shifts, data])
}
return (
<div className="container"
style={containerStyle} >
<div className='secondary_container'>
<Header />
<DataSheet shifts={shifts} addShift={addShift}/>
</div>
</div>
);
}
export default App;
I know it is a lot of code and a lot to ask and I would highly appreciate help with this.
I believe the issue is that you are using setXxx as you would use this.setState. In class components, you have one function that modifies all the state (this.setState), while in function components you have a setter function for each field.
So change this:
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment({ addedAppointment });
}
to this:
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment(addedAppointment);
}
As far as the commitChanges function goes, you can do the data manipulation before using setData. Also I'm not sure that this let { data } = data would work since there is already a data field. You can try this:
const commitChanges = ({ added, changed, deleted }) => {
let newData = [...data.data];
if (added) {
const startingAddedId = newData > 0 ? newData [data.length - 1].id + 1 : 0;
newData = [...newData , { id: startingAddedId, ...added }];
addShift(added);
}
setData(newData);
};
I'm building a help page with Gatsby and have a search bar (Searchbar.js) where I'm trying to pass the user's input in the field (search bar is always present within the page--think like Evernote's help page) to a component that conducts the search (search.js), which then passes that output to the actual results page (SearchResults.js).
When I do gatsby develop everything works as it should, but when I do a gatsby build I get an error where it says it cant read the property "query" because its undefined (line 63 of search.js: var search = location.state.query.trim()). Why is this failing on build?
Searchbar.js
import React from 'react'
import { navigate } from 'gatsby'
import { FaSearch } from 'react-icons/fa'
import searchbarStyles from "./searchbar.module.css"
export default class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
search: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleSubmit(event) {
event.preventDefault()
var query = this.state.search
navigate(
"/search/",
{
state: { query },
}
)
}
handleChange(event) {
this.setState({
search: event.target.value
})
}
render(){
return (
<div className={searchbarStyles.global_search}>
<div className={searchbarStyles.row}>
<form className={searchbarStyles.search} onSubmit={this.handleSubmit}>
<input
type='text'
id='globalSearchInput'
className=''
placeholder='Search Help & Training'
autoComplete='off'
value={this.state.search}
onChange={this.handleChange}
/>
<button
type='submit'
disabled={!this.state.search}
><FaSearch className={searchbarStyles.searchIcon}/></button>
</form>
</div>
</div>
)
}
}
search.js
import React, { useMemo } from 'react'
import Layout from '../components/Layout'
import SearchResults from '../components/SearchResults'
import { graphql } from 'gatsby'
import Fuse from 'fuse.js'
const matchThreshold = .65
//options for the fuzzy search
var fuseOptions = {
shouldSort: true,
threshold: matchThreshold,
location: 0,
distance: 99999999999,
minMatchCharLength: 1,
includeMatches: true,
includeScore: true,
keys: [
{name: "title", weight: 0.3 },
{name: "content", weight: 0.7}
]
};
function cleanString(string) {
const re = /( |<([^>]+)>)/ig
return string.replace(re,'')
}
function FuzzySearch (query, data) {
fuseOptions.minMatchCharLength = query.length
var dataPrepped = data.map(function(element) {
return {
"title": element.node.frontmatter.title,
"content": cleanString(element.node.html),
"slug": element.node.fields.slug,
}
})
var fuse = useMemo(() => new Fuse(dataPrepped, fuseOptions), [])
var results = fuse.search(query)
//customize the results to only return matches within desired threshold
return results.filter(function(match) {
if(match.score <= matchThreshold) {
return true
}
return false
}).map(function(match) {
return {
"title": match.item.title,
"slug": match.item.slug,
"matches": match.matches
}
})
}
export default ({ location, data }) => {
console.log("SERACH.JS\n")
console.log(JSON.stringify(location))
var search = location.state.query.trim()
var results = []
if(search.length) results = FuzzySearch(search, data.allMarkdownRemark.edges)
return (
<Layout>
<SearchResults FoundItems={results} SearchedTerm={search}> </SearchResults>
</Layout>
)
}
export const query = graphql `
query MyQuery {
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
date
doctype
}
html
}
}
}
}
`
SearchResults.js
import React from 'react'
import { Link } from 'gatsby'
import searchResultStyles from "./searchresults.module.css"
function resultsPage(resultsBlurb, results) {
return(
<div className={searchResultStyles.content}>
<h1>Search Results</h1>
<p className={searchResultStyles.resultBlurb}>{resultsBlurb}</p>
<ol>
{results.map((match) => (
<li>
<div className={searchResultStyles.resultContent}>
<Link to={match.slug} className={searchResultStyles.resultTitle}>{match.title}</Link>
{match.matches.map(function(instanceOfMatch) {
if(instanceOfMatch.key === "content") {
let startIndex = instanceOfMatch.indices[0][0]
return(
<p className={searchResultStyles.resultExcerpt}>{`...${instanceOfMatch.value.substring(startIndex, startIndex + 100)}...`}</p>
)
}
})}
</div>
</li>
))}
</ol>
</div>
)
}
class SearchResults extends React.Component {
constructor(props) {
super(props)
this.state = {
results: props.FoundItems,
term: props.SearchedTerm,
}
this.updateBlurb = this.updateBlurb.bind(this)
}
updateBlurb() {
let resultsBlurb = `Sorry--could not find any results for "${this.state.term}"`
if (this.state.results.length) {
resultsBlurb = (this.state.results.length === 1) ? `Only 1 item found for "${this.state.term}"` : `Found ${this.state.results.length} items for "${this.state.term}"`
}
return resultsBlurb
}
componentDidUpdate(prevProps) {
if(prevProps!== this.props) {
this.setState ({
results: this.props.FoundItems,
term: this.props.SearchedTerm
})
}
return(resultsPage(this.updateBlurb(), this.props.FoundItems))
}
render() {
return(resultsPage(this.updateBlurb(), this.state.results))
}
}
export default SearchResults
SOLUTION
(within search.js)
export default ({ location, data }) => {
var search = ''
var results = []
if(typeof window !== "undefiend") search = location.state.query.trim()
if(search.length) results = ...
location is short for window.location, but at build-time your code is running in Node.js which does not have a window. Instead consider testing for the existence of window (typeof window !== "undefined") before running your location.state.query.trim call, and fall back to a default value in the case that window does not exist.
I am simply looking to save and restore a search term(form data) when a page is refreshed/reloaded. I have tried several solutions to no avail.
Flow: A user submits a search term and is taken to Spotify to retrieve an accessToken, if it is not already available. The initial page is refreshed once the accessToken is retrieved, but the search must be re-entered. This is not good UX.
I concluded that Web Storage was they way to go, of course it is not the only route. I am not sure if this is something that should be relegated to Lifecycle methods: componentDidMount() & componentDidUpdate(). Perhaps that is overkill? In any event, I attempted to employ both localStorage and sessionStorage. My implementation is obviously off as I am not getting the expected result. React dev tools displays the state of the SearchBar term, but it is not being saved. Also of note is the following: React dev tools shows that the onSubmit event handler is registering as bound () {} instead of the expected bound handleInitialSearchTerm() {}. The console also shows that there are no errors.
No third-party libraries please.
SearchBar.js
import React from 'react';
import "./SearchBar.css";
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: this.handleInitialSearchTerm
};
this.search = this.search.bind(this);
this.handleInitialSearchTerm = this.handleInitialSearchTerm.bind(this);
this.setSearchTerm = this.setSearchTerm.bind(this);
this.handleSearchOnEnter = this.handleSearchOnEnter.bind(this);
this.handleTermChange = this.handleTermChange.bind(this);
}
handleInitialSearchTerm = (event) => {
if (typeof (Storage) !== "undefined") {
if (localStorage.term) {
return localStorage.term
} else {
return this.setSearchTerm(String(window.localStorage.getItem("term") || ""));
}
}
};
setSearchTerm = (term) => {
localStorage.setItem("term", term);
this.setState({ term: term });
}
search() {
this.props.onSearch(this.state.term);
}
handleSearchOnEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
this.search();
}
}
handleTermChange(event) {
this.setState({
term: event.target.value
});
}
render() {
return (
<div className="SearchBar">
<input
placeholder="Enter A Song, Album, or Artist"
onChange={this.handleTermChange}
onKeyDown={this.handleSearchOnEnter}
onSubmit={this.handleInitialSearchTerm}
/>
<button className="SearchButton" onClick={this.search}>
SEARCH
</button>
</div>
);
}
}
export default SearchBar;
Motify.js
let accessToken;
const clientId = "SpotifyCredentialsHere";
const redirectUri = "http://localhost:3000/";
const CORS = "https://cors-anywhere.herokuapp.com/"; // Bypasses CORS restriction
const Motify = {
getAccessToken() {
if (accessToken) {
return accessToken;
}
// if accessToken does not exist check for a match
const windowURL = window.location.href;
const accessTokenMatch = windowURL.match(/access_token=([^&]*)/);
const expiresInMatch = windowURL.match(/expires_in=([^&]*)/);
if (accessTokenMatch && expiresInMatch) {
accessToken = accessTokenMatch[1]; //[0] returns the param and token
const expiresIn = Number(expiresInMatch[1]);
window.setTimeout(() => accessToken = "", expiresIn * 1000);
// This clears the parameters, allowing us to grab a new access token when it expires.
window.history.pushState("Access Token", null, "/");
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
search(term) {
const accessToken = Motify.getAccessToken();
const url = `${CORS}https://api.spotify.com/v1/search?type=track&q=${term}`;
return fetch(url, { headers: { Authorization: `Bearer ${accessToken}` }
}).then(response => response.json()
).then(jsonResponse => {
if (!jsonResponse.tracks) {
return [];
}
return jsonResponse.tracks.items.map(track => ({
id: track.id,
name: track.name,
artist: track.artists[0].name,
album: track.album.name,
uri: track.uri,
preview_url: track.preview_url
}));
})
}
...
Please check the code I have added.
Changes I did are below:
1)
this.state = {
term: JSON.parse(localStorage.getItem('term')) || '';
};
setSearchTerm = (term) => {
this.setState({
term: term
},
() => {
localStorage.setItem('term', JSON.stringify(this.state.term)));
}
import React from 'react';
import "./SearchBar.css";
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: JSON.parse(localStorage.getItem('term')) || '';
};
this.search = this.search.bind(this);
this.handleInitialSearchTerm = this.handleInitialSearchTerm.bind(this);
this.setSearchTerm = this.setSearchTerm.bind(this);
this.handleSearchOnEnter = this.handleSearchOnEnter.bind(this);
this.handleTermChange = this.handleTermChange.bind(this);
}
handleInitialSearchTerm = (event) => {
if (typeof(Storage) !== "undefined") {
if (localStorage.term) {
return localStorage.term
} else {
return this.setSearchTerm(String(window.localStorage.getItem("term") || ""));
}
}
};
setSearchTerm = (term) => {
this.setState({
term: term
},
() => {
localStorage.setItem('term', JSON.stringify(this.state.term)));
}
search() {
this.props.onSearch(this.state.term);
}
handleSearchOnEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
this.search();
}
}
handleTermChange(event) {
this.setState({
term: event.target.value
});
}
render() {
return ( <
div className = "SearchBar" >
<
input placeholder = "Enter A Song, Album, or Artist"
onChange = {
this.handleTermChange
}
onKeyDown = {
this.handleSearchOnEnter
}
onSubmit = {
this.handleInitialSearchTerm
}
/> <
button className = "SearchButton"
onClick = {
this.search
} >
SEARCH <
/button> <
/div>
);
}
}
export default SearchBar;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>
If it is in hooks i would have done like below:
import React, {
useEffect,
useState,
useRef,
} from 'react';
function App() {
const [value, setValue] = useState(() => {
if (localStorage.getItem('prevCount') === null) {
return 0;
} else {
return localStorage.getItem('prevCount');
}
});
const countRef = useRef();
useEffect(() => {
countRef.current = value;
if (countRef.current) {
localStorage.setItem('prevCount', countRef.current);
} else {
localStorage.setItem('prevCount', 0);
}
});
const handleIncrement = () => {
setValue((value) => +value + 1);
};
const handleDecrement = () => {
if (value === 0) {
return;
} else {
setValue((value) => value - 1);
}
};
return (
<div className="card">
<label className="counterLabel">Simple Counter</label>
<button
className="button"
onClick={handleIncrement}
>
Increment
</button>
<span className="count">{value}</span>
<button
className="button"
onClick={handleDecrement}
>
Decrement
</button>
</div>
);
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>
So what the above code is doing is that when we inititalize the state value we first check the localStorage , if "term" has value in localStorage we will use that value or else an empty string is initialized.
Using callback of setState inside the method setSearchTerm we set the term value immediately
Try the useLocalStorage hook to save search client side.
// useLocalStorage Hook to persist values client side
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
credit: Brandon Baars
import bip39 from 'react-native-bip39';
import bip32 from 'bip32';
import ethUtil from 'ethereumjs-util';
// import { ethers } from 'ethers';
// import { randomBytes } from 'react-native-randombytes'
export class CreateWalletScreen extends Component {
constructor(props) {
super(props);
this.state = {
mnemonic: null,
loading: false
}
}
componentDidMount = () => {
bip39.generateMnemonic().then(mnemonic => {
this.setState({ mnemonic })
});
}
_createWallet = async () => {
const seed = bip39.mnemonicToSeed(this.state.mnemonic);
const root = bip32.fromSeedSync(seed);
const xPrivKey = root.derivePath("m/44'/60'/0'/0/0");
const privKey = xPrivKey.privateKey.toString('hex');
let address = ethUtil.pubToAddress(xPrivKey.publicKey, true).toString('hex');
address = ethUtil.toChecksumAddress(address).toString('hex');
alert(address);
}
render(){
return(
...
<Button onPress={() =>
this.createWallet()} />
...
)}
"react-native-bip39": "^2.3.0",
"bip32": "^2.0.5",
error
enter image description here
Since I'm a beginner, I'm making crypto wallet.
Use the bip library.
Pressing the button should display the address.
But I'm in trouble. damn
If you help me, human peace will come.
plz ...
new error
enter image description here
Your function has to have the same name of the call:
<Button onPress={() =>
this._createWallet()} />